Sample data. Every entry on this page is illustrative.
A ledger of what was tested, and what it proved.
Bravos records each test of a security claim as an entry: the GuardLink annotation that made the claim, the CXG test that ran, the verdict, the evidence, and the commit it ran against.
Below is that ledger for acme-shop, the fictional shop from our homepage story, where Maya and Alex chase an avatar upload that turns into stored XSS.
Three outcomes
- Confirmed
- The test reproduced the exploit against the running app. The risk is real at that commit.
- Refuted
- The test reached the app and the exploit did not work. That rules out one way in, not the threat itself, so it can be tested again from another angle.
- Mitigated
- A security control was seen rejecting the exploit. Stronger than refuted: the defence was observed working, not just the attack failing.
Sample ledger entries
| Entry | Time | Outcome | Claim tested | CXG test | Source | Evidence |
|---|---|---|---|---|---|---|
| Round 1 tested against ff09f3f, the annotated revision of main | ||||||
| L-001 | Refuted | A crafted file name writes outside the uploads folder#path-traversal on #avatar-apihigh severity, CWE-22 | acme-avatar-path-traversal | ff09f3fsample run acme-shop-r1 | ||
VerdictRefuted. path.basename() strips the directory part, so the file stays inside uploads/. GuardLink annotation// @exposes #avatar-api to #path-traversal [high] cwe:CWE-22 -- "x-filename reaches path.join(UPLOAD_DIR, name)" // @comment -- "path.basename() stops directory traversal in x-filename; it does not restrict the file type, so #unrestricted-upload stays open" CXG test
Evidence$ curl -si -X POST http://127.0.0.1:4100/api/avatar -H "x-filename: ../../server.ts" --data-binary "marker"
HTTP/1.1 201 Created
{"url":"/uploads/server.ts"}
$ ls uploads/ # where the file landed
server.ts | ||||||
| L-002 | Confirmed | Any file type is accepted as an avatar#unrestricted-upload on #avatar-apihigh severity, CWE-434 | acme-avatar-html-upload | ff09f3fsample run acme-shop-r1 | ||
VerdictConfirmed. The server stored an HTML file as an avatar. GuardLink annotation// @exposes #avatar-api to #unrestricted-upload [high] cwe:CWE-434 -- "handleAvatarUpload() passes req.headers['x-filename'] to writeFile() with no signature check on the body" // @confirmed #unrestricted-upload on #avatar-api [high] cwe:CWE-434 -- "Local lab at ff09f3f: POST /api/avatar with x-filename: marker-r1.html and an inert HTML body returned 201 and stored /uploads/marker-r1.html" CXG test
Evidence$ curl -si -X POST http://127.0.0.1:4100/api/avatar -H "x-filename: marker-r1.html" --data-binary "<p>acme-shop verification marker</p>"
HTTP/1.1 201 Created
content-type: application/json
{"url":"/uploads/marker-r1.html"} | ||||||
| L-003 | Confirmed | An uploaded HTML file renders in the shop's own origin#stored-xss on #avatar-apihigh severity, CWE-79 | acme-avatar-html-upload | ff09f3fsample run acme-shop-r1 | ||
VerdictConfirmed. The marker came back as text/html from the shop domain. GuardLink annotation// @exposes #avatar-api to #stored-xss [high] cwe:CWE-79 -- "serveUpload() maps TYPES['.html'] to text/html, so an uploaded .html file renders in the shop origin" // @confirmed #stored-xss on #avatar-api [high] cwe:CWE-79 -- "Local lab at ff09f3f: GET /uploads/marker-r1.html returned 200 with content-type: text/html and the marker body" CXG test
Evidence$ curl -si http://127.0.0.1:4100/uploads/marker-r1.html HTTP/1.1 200 OK cache-control: public, max-age=3600 content-type: text/html <p>acme-shop verification marker</p> | ||||||
| L-004 | Refuted | An uploaded SVG runs script when it is viewed#svg-script on #avatar-apimedium severity, CWE-79 | acme-avatar-svg-upload | ff09f3fsample run acme-shop-r1 | ||
VerdictRefuted. The SVG is served as application/octet-stream, so it never renders. GuardLink annotation// @exposes #avatar-api to #svg-script [medium] cwe:CWE-79 -- "An .svg avatar could carry script if it is served as image/svg+xml" // @comment -- "TYPES has no .svg entry, so serveUpload() falls back to application/octet-stream and the browser downloads the file" CXG test
Evidence$ curl -si -X POST http://127.0.0.1:4100/api/avatar -H "x-filename: marker.svg" --data-binary @marker.svg HTTP/1.1 201 Created $ curl -si http://127.0.0.1:4100/uploads/marker.svg HTTP/1.1 200 OK content-type: application/octet-stream | ||||||
| L-005 | Confirmed | One user's upload replaces another user's avatar#avatar-overwrite on #avatar-apimedium severity, CWE-73 | acme-avatar-name-collision | ff09f3fsample run acme-shop-r1 | ||
VerdictConfirmed. The second upload overwrote the first file. GuardLink annotation// @exposes #avatar-api to #avatar-overwrite [medium] cwe:CWE-73 -- "The stored name comes from the client, so two uploads named avatar.png share one file" // @confirmed #avatar-overwrite on #avatar-api [medium] cwe:CWE-73 -- "Sample lab at ff09f3f: a second session's avatar.png replaced the first session's file" CXG test
Evidence$ curl -s -X POST …/api/avatar -H "x-filename: avatar.png" --data-binary @session-a.png # session A
{"url":"/uploads/avatar.png"}
$ curl -s -X POST …/api/avatar -H "x-filename: avatar.png" --data-binary @session-b.png # session B
{"url":"/uploads/avatar.png"}
$ curl -s …/uploads/avatar.png | sha256sum
matches session-b.png | ||||||
| Re-test replayed against e656a86, the fix on fix/avatar-validation | ||||||
| L-006 | Mitigated | Any file type is accepted as an avatar#unrestricted-upload on #avatar-apihigh severity, CWE-434 | acme-avatar-html-upload | e656a86sample replay acme-shop-r2 | ||
VerdictMitigated. The HTML upload is rejected with 415; a real PNG still uploads. GuardLink annotation// @mitigates #avatar-api against #unrestricted-upload using #type-allowlist -- "handleAvatarUpload() accepts only bodies whose leading bytes match SIGNATURES; x-filename is ignored" CXG test
Evidence$ curl -si -X POST http://127.0.0.1:4100/api/avatar -H "x-filename: marker-r2.html" --data-binary "<p>acme-shop verification marker</p>"
HTTP/1.1 415 Unsupported Media Type
{"error":"Avatar must be a PNG or JPEG under 2 MB"}
$ curl -si -X POST http://127.0.0.1:4100/api/avatar --data-binary @tiny.png # control
HTTP/1.1 201 Created | ||||||
| L-007 | Refuted | An uploaded HTML file renders in the shop's own origin#stored-xss on #avatar-apihigh severity, CWE-79 | acme-avatar-html-upload | e656a86sample replay acme-shop-r2 | ||
VerdictRefuted. The file stored before the fix now returns 404. GuardLink annotation// @mitigates #avatar-api against #stored-xss using #type-allowlist -- "serveUpload() only serves image/png or image/jpeg, with X-Content-Type-Options: nosniff" CXG test
Evidence$ curl -si http://127.0.0.1:4100/uploads/marker-r1.html # stored by round 1 HTTP/1.1 404 Not Found | ||||||
| L-008 | Mitigated | One user's upload replaces another user's avatar#avatar-overwrite on #avatar-apimedium severity, CWE-73 | acme-avatar-name-collision | e656a86sample replay acme-shop-r2 | ||
VerdictMitigated. Each session got its own file name; neither upload replaced the other. GuardLink annotation// @mitigates #avatar-api against #avatar-overwrite using #filename-norm -- "Each upload gets its own server-generated name" CXG test
Evidence$ curl -s -X POST …/api/avatar --data-binary @session-a.png # session A
{"url":"/uploads/0b7e…-a1.png"}
$ curl -s -X POST …/api/avatar --data-binary @session-b.png # session B
{"url":"/uploads/5c2d…-f4.png"} | ||||||
| L-009 | Refuted | A very large upload is buffered whole and slows the server#resource-exhaustion on #avatar-apimedium severity, CWE-400 | acme-avatar-oversize | e656a86sample replay acme-shop-r2 | ||
VerdictRefuted. The upload stops at 2 MB and is rejected with 415. GuardLink annotation// @exposes #avatar-api to #resource-exhaustion [medium] cwe:CWE-400 -- "The fix reads the body before checking its signature" // @comment -- "readBody() stops at MAX_BYTES and returns null, so an oversized body is never buffered whole" CXG test
Evidence$ head -c 3145728 /dev/zero | curl -si -X POST …/api/avatar --data-binary @-
HTTP/1.1 415 Unsupported Media Type
{"error":"Avatar must be a PNG or JPEG under 2 MB"} | ||||||
After the re-test at e656a86, this sample has no open exposures on #avatar-api. The next change starts a new round.
Want this ledger for your own code?
Bravos Free keeps a ledger from your first run, with no account. Buy a licence per developer, or seats for your team, to run it on every pull request. Every entry it records links back to the annotation, the test and the commit.