Skip to content
Use case 01 / Cybersecurity research

Keep the research moving.

Read the handler, name the sink, write the diff, draft the rule, table the cases. For authorized work on systems you own or are engaged to test.

Authorized scope onlysystems you own or test
262,144tokens of context
off / low / xhighreasoning effort
Text outno execution, ever
Research note / file service

A path traversal, from reading to test table.

Three steps on one function. The interesting step is the third — the rows that most patches never get tested against.

Step 01 — analysisread

The join that does nothing

os.path.join does not sanitize. It concatenates, and only the first argument is treated as a root — until the second argument starts with a slash, at which point the root is discarded entirely.

So read_report("../../etc/passwd") becomes "/srv/reports/../../etc/passwd", and open() does the rest. Nothing in the function checks that the resolved path ever stayed under /srv/reports.

The reading to write into the ticket: user input name reaches the open() sink with no containment check; the bug is CWE-22, path traversal. That sentence is the whole finding. Everything after it is proof.

reports.py· under review
# reports.py — the version under review
import os

BASE = "/srv/reports"

def read_report(name: str) -> bytes:
    path = os.path.join(BASE, name)
    with open(path, "rb") as fh:
        return fh.read()
Step 02 — patchdiff

Resolve, then compare components

realpath resolves .. and follows symlinks before the comparison happens, so the check is applied to the path that will actually be opened.

commonpath compares path components, not string prefixes. A prefix check — path.startswith(BASE) — passes /srv/reports-archive/x, which is a sibling directory, not a child. That is the difference between closing the class of bug and closing the one string you thought of.

Note what this does not fix: between the check and the open() there is still a window. If another process can replace a component with a symlink inside that window, the resolution is stale. Closing the race means openat with O_NOFOLLOW or an already-held directory descriptor — a different patch, for a different threat model. Say which one you wrote.

reports.py· proposed patch
--- a/reports.py
+++ b/reports.py
@@ -1,9 +1,14 @@
 import os
 
-BASE = "/srv/reports"
+BASE = os.path.realpath("/srv/reports")
 
 def read_report(name: str) -> bytes:
-    path = os.path.join(BASE, name)
+    path = os.path.realpath(os.path.join(BASE, name))
+    if os.path.commonpath([BASE, path]) != BASE:
+        raise ValueError("path outside report root")
     with open(path, "rb") as fh:
         return fh.read()
Step 03 — teststhe table

The rows a patch has to survive

A patch is a hypothesis. The table is how it becomes a fact. These are the inputs that separate a real containment check from a check that only caught the string from the report.

Two of the rows below do not produce the answer you might expect on a first read. Keep them. A test table where everything passes the way you assumed is a test table that was written by the same person as the patch.

inputwhat resolvesexpected
q3-2025.pdf inside the root; commonpath is BASE returned
../../etc/passwd resolves to /etc/passwd; commonpath is / rejected
/etc/passwd absolute second argument discards BASE in join rejected
sub/2025/q3.pdf two levels down, still a child of the root returned
link-escape symlink inside the root pointing at /etc/shadow; resolution follows it rejected
..%2f..%2fetc%2fpasswd decoding happens in whatever sits in front of you; by the time the function sees it, it is row 2 rejected
q3-2025.pdf\x00 embedded null; realpath raises before the guard runs raises
. resolves to the root itself; the guard passes, open() raises IsADirectoryError raises

Rows 7 and 8 are the useful ones. The guard passes and something downstream fails. That is not a hole — it is an error path you now know about, and the difference between "we block traversal" and "we block traversal, and a directory request is a 500 unless someone handles it". Write the second sentence in the ticket.

what the model produced, and what it did not
Produced
The reading, the diff, the table

Three artefacts, in the shapes you would hand to a reviewer: a data-flow sentence, a patch against the file you pasted, and rows with expected outcomes.

Not produced
Evidence that any of it works

Nothing was executed. The model has never run realpath on your filesystem, and it cannot tell you whether your Python version behaves the way the snippet assumes.

Yours to do
Run it, break it, retest

Apply the patch on a branch, run the table as a real test file, confirm the rejection is a rejection and not an exception that happens to look like one.

Workflow 04, made concrete

The same finding, as a rule.

A patch closes the instance. A rule buys you notice if something else in the fleet is still running the vulnerable build — and tells you who is trying.

Draft the rule against the log line you actually have. Paste one real request from the access log, not a description of it: field names differ between nginx, ALB, and whatever the proxy in the middle normalises, and a rule keyed to the wrong field is a rule that never fires.

Then tune it. Run the draft against a day of your own traffic before enabling it, and read the false positives — they tell you which encodings your stack already decodes before the rule sees them.

The traversal rows in the table above and this rule describe the same event from two ends: one is a unit test, the other is a monitor. Neither replaces the other.

sigma· draft, untested
title: Traversal attempt against report download
id: 3b9f6a12-0c47-4d55-9a0e-8f21c7d0be31
status: experimental
description: Parent-directory sequences on /reports/* downloads.
logsource:
  category: webserver
detection:
  route:
    cs-uri-stem|startswith: '/reports/'
  traversal:
    cs-uri-query|contains:
      - '../'
      - '..%2f'
      - '%2e%2e%2f'
      - '....//'
  filter_internal:
    c-ip|startswith: '10.0.0.'
  condition: route and traversal and not filter_internal
fields: [c-ip, cs-method, cs-uri-stem, cs-uri-query, sc-status]
falsepositives:
  - Legacy client that URL-encodes a literal filename.
level: high
tags:
  - attack.initial_access
  - attack.t1190
Six workflows

Where it earns the afternoon back.

Each row is a draft loop: you paste the artefact, you get a draft back, you check it against reality.

01

Vulnerability analysis

Paste the handler, the route, or the parser. The draft comes back as a reading rather than a lecture: the tainted input, the sink it reaches, the guard that is not there, the class of bug, and the ticket text a reviewer will accept without asking you to redo it.

in handler · model · template  →  out data-flow note, CWE, ticket draft

02

Patch review

Put the diff and the original finding in the same message. The question worth asking is whether the patch closed the class or the instance — absolute paths, symlinks, unicode normalisation, Windows separators, and the second call site that was never updated are where instance patches fall over.

in diff + prior finding  →  out gap list, revised hunks, missing-call-site list

03

Test cases

Inputs and expected outcomes, written for the runner you already use. The value is concentrated in the rejects: traversal, XXE, SSRF, and deserialisation bugs survive a patch mostly because nobody wrote the row that fails.

in function + language + framework  →  out case table, pytest/unittest skeletons

04

Detection rules

Sigma for the log event, YARA for the sample, Suricata for the wire. The draft is only as good as your sample — paste the actual log line, not a description of it, and ask for the false-positive notes alongside the rule.

in log sample · strings · pcap notes  →  out rule draft, FP notes, tuning fields

05

Malware & binary triage

Strings, imports, config blobs, persistence hypotheses, ATT&CK mapping from what was actually observed. Sandbox screenshots and disassembly excerpts go in as input; a written triage note comes back out.

in strings, imports, capa output, screenshot  →  out triage note, ATT&CK candidates, IOCs to check

06

Engagement reports

Findings shaped for the template your client already expects: summary, affected component, reproduction steps, CVSS vector with the reasoning behind each metric, remediation advice, and the retest criteria that decides whether the finding closes.

in notes, evidence, template  →  out finding, vector justification, retest list

Two more readings

Short functions, familiar bugs.

Same pattern as the traversal: the code is unremarkable, the sink is one line away, and the fix has a wrong version that looks right.

SSRF / preview helper
preview.py· 4 lines
def preview(url: str) -> str:
    r = requests.get(url, timeout=5)
    return r.text[:2000]

No scheme check, no host allowlist, redirects followed by default, and DNS resolving whatever the user typed — including a link-local metadata address. A regex that blocks localhost is not a fix; the attacker writes the IP.

The reading to write down: user-controlled URL reaches an outbound request; SSRF, CWE-918. The fix is a scheme allowlist, an IP check after resolution on the pinned address, redirects disabled, and a streamed size cap.

SSTI / template render
mailer.py· 1 line
return render_template_string(user_template, rows=rows)

Rendering a template the user supplied is server-side template injection, not a formatting convenience. Sanitising the string with a regex does not close it — the expression syntax is the feature.

Two honest fixes: a SandboxedEnvironment with a restricted context, or a static template that takes the user's values as data. The second one is the patch a reviewer will accept without an argument.

Notice what the model does with the fix. It offers the wrong version too, and you can ask for it: "what is the fix that a rushed engineer writes here, and why does it fail?" That question is worth asking before review, not during it.

Workflow 06, made concrete

What a finding looks like when it closes.

A finding is not the paragraph describing the bug. It is the paragraph, the vector with its reasoning, the remediation, and the retest criteria that decide whether anyone can call this done.

Ask for the CVSS reasoning explicitly. "High severity" is an opinion; AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N is an argument you can be asked to defend, metric by metric, in a client call.

The retest line is the part people skip. Write what would have to be true for the finding to close — a specific input that must be rejected, not a code review.

findings/F-002.md· draft
### F-002 — Path traversal in report download
**Component:** reports.py / read_report()
**Class:** CWE-22 · **Severity:** high
**Vector:** CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
**Why:** network-reachable with no auth on the route,
no interaction required, read-only impact on
arbitrary files readable by the service account.

**Repro:** GET /reports?name=../../etc/passwd
        returns the first line of the file, HTTP 200.

**Fix:** resolve, then compare components
  path = realpath(join(BASE, name))
  if commonpath([BASE, path]) != BASE: reject

**Not fixed by:** blocking the string '../' or a
startswith() prefix check on BASE alone.

**Retest:** the eight rows in the case table, run
against the patched build; all rejections must be
rejections, not unhandled exceptions.
We are not your pentester. We do not run scans against your systems. We do not validate a patch by executing it. We do not touch your infrastructure, your credentials, or your network. We do not write authorisation and we cannot give it.

You bring the engagement: a system you own, or a scope document that names you and the testing window. You run whatever a draft proposes, on the machine you control. You verify that it worked. If a draft suggests something outside your scope, the answer is no — and the model produced a bad draft, which is a normal thing for a text generator to do.

Everything on this page assumes authorized security work. If you do not have permission for the system in the prompt, you are not the user this endpoint is for, and no model's willingness changes that.

Honest limits

Where it will let you down.

Six ways this fails. Knowing them is cheaper than discovering them at 4pm on a Friday.

Limit 01
No live data

There is no CVE feed, no vendor advisory, and no internet behind the endpoint. Given a version number it will produce a confident CVE identifier that may well be wrong. Check every identifier before it enters a report.

Limit 02
It cannot run or compile

No interpreter, no linker, no shell. A snippet that looks correct on screen can be wrong in a passing way: wrong import, wrong signature, a decorator that changes what the function returns. Compile it yourself.

Limit 03
Version drift

How a library behaves in the exact version you deploy is your field knowledge, not its memory. realpath semantics differ at the edges; framework routers normalise paths before your code sees them. Test the version you ship.

Limit 04
Fluency is not evidence

The prose is smooth whether or not the finding is real. A wrong analysis reads exactly like a correct one. Treat every claim about your codebase as a hypothesis with a test attached, or do not use it.

Limit 05
No transcript to check later

Zero retention means the prompts are never written to disk. That is the point, and it also means there is no history to scroll back through. Save what matters in your own notes as you go.

Limit 06
No moderation, no rescue

We do not review, monitor, or filter prompts. Nothing here will stop a bad request or flag one for follow-up. The judgement about what belongs in a prompt is entirely on the operator — you.

The call

No SDK, no integration, no plugin.

One POST. The tooling you already have is the tooling you use — curl, a script, or the playground in a tab.

shell· one request
curl "$BASE_URL/v1/chat/completions" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen-flash-next",
    "effort": "low",
    "messages": [
      { "role": "user",
        "content": "Read this handler. Name the sink,"
                 + " the missing guard, and draft the diff." }
    ]
  }'
200· response shape
{
  "id": "chatcmpl-9f2c…",
  "object": "chat.completion",
  "model": "qwen-flash-next",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "The sink is open() on line 8…"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 1840,
    "completion_tokens": 612,
    "total_tokens": 2452
  }
}
One model string

qwen-flash-next is the only value that has ever been valid. There is no second model to fall back to and no alias that resolves to something else.

Three routes

/v1/chat/completions, /v1/models, /v1/usage. Nothing for embeddings, batches, files, assistants, or fine-tuning.

Counters only

Tokens, requests, timestamp, per key. The prompt is not part of the record, which is also why /v1/usage can show you spend but never history.

FAQ

The four questions that come up.

Is it legal for me to use this?

Depends on where you live, what you are doing, and who you work for. We do not verify, we do not ask, and we do not provide authorization — that document comes from the system owner, not from a model vendor.

The assumption this page runs on is narrow: you are testing systems you own, or systems you are engaged in writing to test. If that is not true for the prompt in front of you, a refusal from some other model would not have made it legal either.

Do you keep the code I paste?

No. Prompts and completions are never written to disk, never logged, and never trained on. What is retained is a per-key counter: tokens, requests, timestamp. That is the whole record, and it is not enough to reconstruct a request.

The practical consequence is the one people forget: we cannot produce your prompt later, because we never had it. If you need a record, keep one.

Will it actually draft the intrusion-relevant part, or hedge?

It complies. HarmBench-320 TRUE_COMPLY sits at 91.2% at effort off and 92.5% at low, with zero hard refusals across all 320 prompts. Cybercrime and intrusion-compliance prompts were tested separately: 52 of 52 at off and at low.

There is no refusal layer to route around, because the refusals were removed at the weight level. What you get back is the draft — which means the judgement about scope has to sit with you, because nothing in the pipeline is going to add it.

Vendor-reported. Model card, agentic harness, checked 2026-11. Not a measurement of this endpoint.

How much code fits in one request?

262,144 tokens of context natively, extensible toward 1M, with 131,072 tokens of output. A few hundred files fit. That is not the same as a good reading: the further a file sits from the question, the softer the answer.

The pattern that works is one file plus its caller, then a second pass with the file it calls. Keep the reasoning effort readable — off for prose and refactors, low for analysis, xhigh when the trace itself is the deliverable.

Next

Bring the finding.
Bring the authorization.

Fifty a month, unlimited. Crypto only, no logs. Try it for $5 first — the same key covers every page in this section.

You are the operator. Everything that follows is yours.