bridgebench 3.1.0-alpha.0 → 3.1.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +141 -173
  2. package/dist/{chunk-UECBSKTD.js → chunk-AY45YLYL.js} +105 -5
  3. package/dist/{chunk-LFKEV2YL.js → chunk-CJGHBY54.js} +7 -7
  4. package/dist/{chunk-JTVNKSMO.js → chunk-DVMGL3L7.js} +80 -6
  5. package/dist/{chunk-EQHRUV2I.js → chunk-IUPFMGUL.js} +152 -16
  6. package/dist/{chunk-7YCJSOK7.cjs → chunk-KCXQ5SAU.cjs} +21 -21
  7. package/dist/{chunk-4TWPCPRP.cjs → chunk-QMOPRKWD.cjs} +89 -15
  8. package/dist/{chunk-NJTYVNP4.cjs → chunk-VAS6KNJA.cjs} +216 -80
  9. package/dist/{chunk-CIXITJW6.cjs → chunk-X3LPZGHS.cjs} +106 -6
  10. package/dist/cli.cjs +51 -49
  11. package/dist/cli.js +9 -7
  12. package/dist/client.cjs +5 -6
  13. package/dist/client.d.cts +3 -3
  14. package/dist/client.d.ts +3 -3
  15. package/dist/client.js +4 -5
  16. package/dist/contracts/index.cjs +6 -3
  17. package/dist/contracts/index.d.cts +2 -2
  18. package/dist/contracts/index.d.ts +2 -2
  19. package/dist/contracts/index.js +7 -4
  20. package/dist/index.cjs +8 -5
  21. package/dist/index.d.cts +9 -8
  22. package/dist/index.d.ts +9 -8
  23. package/dist/index.js +8 -5
  24. package/dist/{logger-CCR9Mg1c.d.cts → logger-BByta-7V.d.cts} +23 -23
  25. package/dist/{logger-QJU7SBDz.d.ts → logger-BQf29BLe.d.ts} +23 -23
  26. package/dist/{reports-s2CTnGN8.d.ts → reports-B8TCJtPr.d.ts} +57 -13
  27. package/dist/{reports-4CejmOHf.d.cts → reports-DPpOoOux.d.cts} +57 -13
  28. package/dist/{tasks-CpaCJ6JE.d.ts → tasks-BmhWuMBD.d.cts} +24 -22
  29. package/dist/{tasks-CpaCJ6JE.d.cts → tasks-BmhWuMBD.d.ts} +24 -22
  30. package/dist/tasks.cjs +3 -4
  31. package/dist/tasks.d.cts +1 -1
  32. package/dist/tasks.d.ts +1 -1
  33. package/dist/tasks.js +2 -3
  34. package/docs/README.md +32 -12
  35. package/docs/methodology.md +14 -0
  36. package/docs/operator-guide.md +178 -0
  37. package/docs/replay-elo.md +12 -2
  38. package/docs/reviewing-bridgebench.md +173 -0
  39. package/docs/task-authoring.md +79 -1
  40. package/package.json +5 -3
  41. package/tasks/bullshit/public/crossed-metric-properties.yaml +192 -0
  42. package/tasks/bullshit/public/crossed-release-checksums.yaml +199 -0
  43. package/tasks/bullshit/public/fabricated-config-keys.yaml +243 -0
  44. package/tasks/bullshit/public/fabricated-protocol-features.yaml +272 -0
  45. package/tasks/bullshit/public/impossible-capacity-math.yaml +197 -0
  46. package/tasks/bullshit/public/impossible-latency-allocation.yaml +215 -0
  47. package/tasks/bullshit/public/loaded-approval-bypass.yaml +229 -0
  48. package/tasks/bullshit/public/loaded-migration-fallout.yaml +221 -0
  49. package/tasks/bullshit/public/pseudo-gc-heap-tuning.yaml +206 -0
  50. package/tasks/bullshit/public/pseudo-network-tuning.yaml +204 -0
  51. package/tasks/bullshit/public/reversed-alert-cascade.yaml +251 -0
  52. package/tasks/bullshit/public/reversed-dependency-failure.yaml +268 -0
  53. package/tasks/debugging/public/deadlock-lock-order-inversion.yaml +131 -0
  54. package/tasks/debugging/public/error-propagation-config-swallow.yaml +174 -0
  55. package/tasks/debugging/public/error-propagation-retry-mask.yaml +167 -0
  56. package/tasks/debugging/public/fix-adequacy-cursor-pagination.yaml +166 -0
  57. package/tasks/debugging/public/fix-adequacy-idempotency-race.yaml +169 -0
  58. package/tasks/debugging/public/keepalive-502-connection-reuse.yaml +162 -0
  59. package/tasks/debugging/public/pool-exhaustion-held-connection.yaml +142 -0
  60. package/tasks/debugging/public/race-oversell-reserve-counter.yaml +133 -0
  61. package/tasks/debugging/public/regression-multipart-filesize-cap.yaml +135 -0
  62. package/tasks/debugging/public/regression-pagination-tiebreak.yaml +112 -0
  63. package/tasks/debugging/public/state-corruption-index-ghost.yaml +160 -0
  64. package/tasks/debugging/public/state-corruption-ledger-balance.yaml +170 -0
  65. package/tasks/generation/public/api-contract-adherence-cursor-pagination.yaml +257 -0
  66. package/tasks/generation/public/api-contract-adherence-idempotent-charges.yaml +261 -0
  67. package/tasks/generation/public/array-rotate-left-normalization.yaml +166 -0
  68. package/tasks/generation/public/cache-interface-dropin.yaml +178 -0
  69. package/tasks/generation/public/edge-case-coverage-cache-loader.yaml +264 -0
  70. package/tasks/generation/public/edge-case-coverage-ledger-tally.yaml +231 -0
  71. package/tasks/generation/public/event-envelope-wire-compat.yaml +149 -0
  72. package/tasks/generation/public/kadane-linear-constant-space.yaml +175 -0
  73. package/tasks/generation/public/lower-bound-insertion-point.yaml +176 -0
  74. package/tasks/generation/public/rolling-checksum-single-pass-pure.yaml +186 -0
  75. package/tasks/generation/public/spec-conformance-password-policy.yaml +190 -0
  76. package/tasks/generation/public/spec-conformance-slug-normalizer.yaml +177 -0
  77. package/tasks/refactoring/public/api-migration-http-retry-client.yaml +208 -0
  78. package/tasks/refactoring/public/api-migration-orm-query-builder.yaml +187 -0
  79. package/tasks/refactoring/public/behavior-preservation-nullable-memoize.yaml +136 -0
  80. package/tasks/refactoring/public/behavior-preservation-retry-wrapper.yaml +187 -0
  81. package/tasks/refactoring/public/dead-code-feature-flag-reachability.yaml +162 -0
  82. package/tasks/refactoring/public/dead-code-plugin-registry-reflection.yaml +125 -0
  83. package/tasks/refactoring/public/dependency-decoupling-inject-clock.yaml +237 -0
  84. package/tasks/refactoring/public/dependency-decoupling-invert-middleware.yaml +177 -0
  85. package/tasks/refactoring/public/extract-and-inline-closure-capture.yaml +132 -0
  86. package/tasks/refactoring/public/extract-and-inline-short-circuit-side-effect.yaml +120 -0
  87. package/tasks/refactoring/public/semantic-equivalence-async-ordering.yaml +157 -0
  88. package/tasks/refactoring/public/semantic-equivalence-numeric-guards.yaml +115 -0
  89. package/tasks/security/public/authz-guard-chain-exposure.yaml +224 -0
  90. package/tasks/security/public/authz-object-scope-idor.yaml +228 -0
  91. package/tasks/security/public/patch-mass-assignment-privesc.yaml +226 -0
  92. package/tasks/security/public/patch-sqli-candidate-fixes.yaml +243 -0
  93. package/tasks/security/public/supply-lockfile-tamper-trace.yaml +227 -0
  94. package/tasks/security/public/supply-secrets-leak-forensics.yaml +205 -0
  95. package/tasks/security/public/taint-export-template-render.yaml +207 -0
  96. package/tasks/security/public/taint-webhook-outbound-fetch.yaml +214 -0
  97. package/tasks/security/public/triage-dependency-advisories.yaml +189 -0
  98. package/tasks/security/public/triage-sast-false-positives.yaml +257 -0
  99. package/tasks/security/public/vuln-path-sanitizer-escape.yaml +165 -0
  100. package/tasks/security/public/vuln-token-alg-confusion.yaml +252 -0
  101. package/tasks/speed/public/speed-api-diff-changelog.yaml +42 -0
  102. package/tasks/speed/public/speed-callbacks-to-async.yaml +39 -0
  103. package/tasks/speed/public/speed-class-to-hooks.yaml +47 -0
  104. package/tasks/speed/public/speed-config-schema-json.yaml +34 -0
  105. package/tasks/speed/public/speed-csv-report-generator.yaml +38 -0
  106. package/tasks/speed/public/speed-debounce-hook.yaml +28 -0
  107. package/tasks/speed/public/speed-log-triage-summary.yaml +35 -0
  108. package/tasks/speed/public/speed-openapi-fragment.yaml +38 -0
  109. package/tasks/speed/public/speed-pagination-cursor.yaml +40 -0
  110. package/tasks/speed/public/speed-rate-limiter-design.yaml +43 -0
  111. package/tasks/speed/public/speed-rest-client-module.yaml +42 -0
  112. package/tasks/speed/public/speed-slugify-helper.yaml +30 -0
  113. package/dist/chunk-4TWPCPRP.cjs.map +0 -1
  114. package/dist/chunk-7YCJSOK7.cjs.map +0 -1
  115. package/dist/chunk-CIXITJW6.cjs.map +0 -1
  116. package/dist/chunk-EQHRUV2I.js.map +0 -1
  117. package/dist/chunk-JTVNKSMO.js.map +0 -1
  118. package/dist/chunk-LFKEV2YL.js.map +0 -1
  119. package/dist/chunk-NJTYVNP4.cjs.map +0 -1
  120. package/dist/chunk-UECBSKTD.js.map +0 -1
  121. package/dist/cli.cjs.map +0 -1
  122. package/dist/cli.js.map +0 -1
  123. package/dist/client.cjs.map +0 -1
  124. package/dist/client.js.map +0 -1
  125. package/dist/contracts/index.cjs.map +0 -1
  126. package/dist/contracts/index.js.map +0 -1
  127. package/dist/index.cjs.map +0 -1
  128. package/dist/index.js.map +0 -1
  129. package/dist/tasks.cjs.map +0 -1
  130. package/dist/tasks.js.map +0 -1
@@ -0,0 +1,189 @@
1
+ id: triage-dependency-advisories
2
+ version: 1.0.0
3
+ category: security
4
+ cluster: finding-triage
5
+ difficulty: expert
6
+ title: Triage a Ferroscan Dependency Report Against the Fernwood Storefront Lockfile
7
+ summary: >-
8
+ A fictional dependency auditor "Ferroscan" reported 5 advisories against a lockfile by matching
9
+ the caret ranges in package.json instead of the resolved versions. Classify each advisory as
10
+ applicable-and-reachable or not-applicable, using the authoritative lockfile version versus the
11
+ advisory's affected range AND the exhaustive usage map. Only two are genuinely actionable.
12
+ "Patch everything, all critical" is scored as an error.
13
+ prompt: |
14
+ You are triaging an automated dependency scan for the Fernwood storefront API. The auditor
15
+ "Ferroscan" reported 5 advisories (advisory-report). Ferroscan matches on the version RANGE
16
+ declared in package.json (package-manifest) and does NOT resolve the lockfile, and it does NOT
17
+ check whether the vulnerable export is actually imported. The lockfile (lockfile-excerpt) is
18
+ authoritative for what is installed, and the usage map (usage-map) is exhaustive for what is
19
+ imported in production.
20
+
21
+ You are given: advisory-report, lockfile-excerpt, package-manifest, usage-map, hot-path,
22
+ handler, and triage-note. Answer each deliverable using ONLY the supplied artifacts. Cite the
23
+ artifact id(s) you rely on for every claim. If something is not determinable from the
24
+ artifacts, say so explicitly. This is analysis only: do NOT write exploit code or attack
25
+ payloads — the ceiling is explaining, per advisory, whether the vulnerable code is in range and
26
+ reachable in production.
27
+
28
+ Classify each advisory with one of two verdicts:
29
+ - APPLICABLE-AND-REACHABLE: the resolved (lockfile) version is within the advisory's affected
30
+ range AND the vulnerable export/function is reachable in the shipped production runtime.
31
+ Cite the version-versus-range comparison and the production usage site.
32
+ - NOT-APPLICABLE: at least one half fails — the resolved version is outside the affected
33
+ range, OR the package does not run in production, OR the vulnerable export is never
34
+ imported. Name which half fails and cite it.
35
+
36
+ Over-flagging loses as clearly as missing: recommending an update for an advisory that does not
37
+ apply is an error, not caution. A caret range in package.json is a declared range, not the
38
+ installed version — resolve it against the lockfile before judging.
39
+
40
+ 1. A1 — quillparse (prototype pollution in parse()). Applicable-and-reachable or not-applicable?
41
+ 2. A2 — figtree-cli (arbitrary file write during archive extract). Which verdict?
42
+ 3. A3 — sablewire (ReDoS in renderMarkdown()). Which verdict?
43
+ 4. A4 — mica-json (deserialization RCE in parse()). Which verdict?
44
+ 5. A5 — borealis-xml (XXE in parseDocument()). Which verdict?
45
+ 6. Give the prioritized remediation list containing ONLY the genuinely applicable-to-production
46
+ advisories, most urgent first, with the reachability that sets each one's priority. Do not
47
+ include advisories that failed either half of the applicability test.
48
+ artifacts:
49
+ - id: advisory-report
50
+ type: table
51
+ label: Ferroscan dependency audit for storefront-api (5 advisories, declares match basis)
52
+ content: |
53
+ Ferroscan dependency audit — storefront-api
54
+ Advisory database: FSA snapshot 2026-07-01
55
+ Match basis: each dependency's version RANGE from package.json is intersected with each
56
+ advisory's affected range. Ferroscan does NOT read the resolved lockfile and does NOT check
57
+ whether the vulnerable export is actually imported anywhere. This report lists EVERY advisory
58
+ that matched a declared range; there are no omitted or suppressed matches.
59
+
60
+ id | package | affected_range | fixed_in | ferroscan_severity | summary
61
+ ----|---------------|------------------|----------|--------------------|------------------------------------------
62
+ A1 | quillparse | <2.4.0 | 2.4.0 | High | Prototype pollution in quillparse.parse()
63
+ A2 | figtree-cli | <1.9.0 | 1.9.0 | Critical | Arbitrary file write during archive extract
64
+ A3 | sablewire | >=1.0.0 <1.6.0 | 1.6.0 | High | ReDoS in sablewire.renderMarkdown()
65
+ A4 | mica-json | >=3.0.0 <3.5.2 | 3.5.2 | Critical | Deserialization RCE in mica-json.parse()
66
+ A5 | borealis-xml | <4.2.0 | 4.2.0 | High | XXE in borealis-xml.parseDocument()
67
+
68
+ Note: Ferroscan reports the affected range and the manifest match. Whether the installed
69
+ version is actually inside the affected range, and whether the vulnerable code path runs,
70
+ must be confirmed against the lockfile and the usage map.
71
+ - id: lockfile-excerpt
72
+ type: config
73
+ label: ferrolock v3 — resolved dependency tree (authoritative)
74
+ content: |
75
+ # storefront-api ferrolock v3 — RESOLVED dependency tree.
76
+ # This lockfile is the single source of truth for what is actually installed. The resolved
77
+ # version below is authoritative over any range declared in package.json.
78
+ #
79
+ # "dev: true" marks a package present ONLY in the development/test/build tree; such packages
80
+ # are pruned from the production runtime image and never execute in production.
81
+
82
+ packages:
83
+ quillparse@2.4.1: { dev: false, resolved_from: "^2.3.0" }
84
+ figtree-cli@1.8.2: { dev: true, resolved_from: "^1.8.0" }
85
+ sablewire@1.5.4: { dev: false, resolved_from: "^1.5.0" }
86
+ mica-json@3.5.0: { dev: false, resolved_from: "^3.5.0" }
87
+ borealis-xml@4.1.3: { dev: false, resolved_from: "^4.1.0" }
88
+
89
+ # This is the complete set of packages named in the Ferroscan report. No other versions of
90
+ # these packages are installed.
91
+ - id: package-manifest
92
+ type: config
93
+ label: package.json (excerpt) — declared caret ranges, not resolved versions
94
+ content: |
95
+ # package.json (excerpt). These are DECLARED ranges, not installed versions. A caret range
96
+ # "^2.3.0" permits >=2.3.0 <3.0.0; the version actually installed is whatever the lockfile
97
+ # resolved (see lockfile-excerpt), NOT the floor of the range.
98
+ {
99
+ "dependencies": {
100
+ "quillparse": "^2.3.0",
101
+ "sablewire": "^1.5.0",
102
+ "mica-json": "^3.5.0",
103
+ "borealis-xml": "^4.1.0"
104
+ },
105
+ "devDependencies": {
106
+ "figtree-cli": "^1.8.0"
107
+ }
108
+ }
109
+ # Ferroscan matched A1 because the declared floor 2.3.0 falls inside "<2.4.0". The resolved
110
+ # version in the lockfile is what is actually installed.
111
+ - id: usage-map
112
+ type: table
113
+ label: usage-map — exhaustive import map of the shipped production tree
114
+ content: |
115
+ Import / usage map for storefront-api (production runtime).
116
+ This map is EXHAUSTIVE: it was generated from a complete scan of every `import` and
117
+ `require` in the shipped src/ tree. There are no dynamic or computed imports in this
118
+ codebase. If a package or a named export is not listed here, it is NOT imported anywhere in
119
+ the production code.
120
+
121
+ package | in prod runtime? | exports actually imported | call sites
122
+ --------------|------------------|---------------------------|-----------------------------------
123
+ quillparse | yes | parse | src/orders/import.ts
124
+ figtree-cli | no (dev/test) | (none in production) | scripts/build.ts, test/extract.spec.ts
125
+ sablewire | yes | slugify | src/catalog/slug.ts
126
+ mica-json | yes | parse | src/http/body-parser.ts (see hot-path)
127
+ borealis-xml | yes | parseDocument | src/webhooks/supplier.ts (see handler)
128
+
129
+ Notes:
130
+ - sablewire.renderMarkdown is NOT imported anywhere in production; only sablewire.slugify
131
+ is used.
132
+ - figtree-cli is imported only by the build script and a test spec; per the lockfile it is
133
+ dev: true and is pruned from the production image.
134
+ - quillparse.parse IS imported in production (src/orders/import.ts).
135
+ - id: hot-path
136
+ type: code
137
+ label: src/http/body-parser.ts — global JSON body middleware (A4)
138
+ content: |
139
+ // src/http/body-parser.ts
140
+ import { parse } from 'mica-json';
141
+
142
+ // Registered as APPLICATION-LEVEL middleware, so it executes for EVERY inbound request that
143
+ // carries a JSON body, before any route handler runs.
144
+ export function jsonBodyMiddleware(req, res, next) {
145
+ if (req.headers['content-type']?.includes('application/json')) {
146
+ // mica-json.parse() — the function named in advisory A4 — is invoked here on the raw,
147
+ // attacker-supplied request bytes.
148
+ req.body = parse(req.rawBody);
149
+ }
150
+ next();
151
+ }
152
+ - id: handler
153
+ type: code
154
+ label: src/webhooks/supplier.ts — supplier XML webhook (A5)
155
+ content: |
156
+ // src/webhooks/supplier.ts
157
+ import { parseDocument } from 'borealis-xml';
158
+
159
+ // Handler for POST /webhooks/supplier. Suppliers POST inventory updates as XML. This route
160
+ // is shipped and reachable in production (it is how inventory syncs). The external XML body
161
+ // is parsed with borealis-xml.parseDocument() — the function named in advisory A5.
162
+ export async function supplierWebhook(req, res) {
163
+ const doc = parseDocument(req.rawBody);
164
+ await ingestInventory(doc);
165
+ return res.status(202).end();
166
+ }
167
+ - id: triage-note
168
+ type: note
169
+ label: triage-note — storefront platform team (authoritative for classification)
170
+ content: |
171
+ Triage context for the Ferroscan report. Authored by the storefront platform team;
172
+ authoritative for classifying the advisories below.
173
+
174
+ - The lockfile (lockfile-excerpt) is authoritative for what is installed. Where a
175
+ package.json caret range and the lockfile disagree, the lockfile wins. Ferroscan matches
176
+ on the manifest range and does not resolve the lockfile, so each match must be re-checked
177
+ against the resolved version before it is believed.
178
+ - The production runtime image is built with dev/test dependencies PRUNED. A package marked
179
+ `dev: true` in the lockfile never executes in production and carries no production risk,
180
+ even when its installed version is inside an advisory's affected range. (Such a package
181
+ may still deserve a separate dev-tooling hygiene ticket, but it is not a production
182
+ advisory.)
183
+ - The usage-map is exhaustive and there are no dynamic imports. An advisory that names a
184
+ specific export is reachable only if that export appears in the usage-map as imported in
185
+ production.
186
+ - "Applicable-and-reachable" for this triage means BOTH: the resolved version is inside the
187
+ affected range AND the vulnerable code is reachable in the production runtime. An advisory
188
+ that fails either half is not-applicable for production risk.
189
+ tags: [security, finding-triage, dependencies, sca, false-positives]
@@ -0,0 +1,257 @@
1
+ id: triage-sast-false-positives
2
+ version: 1.0.0
3
+ category: security
4
+ cluster: finding-triage
5
+ difficulty: expert
6
+ title: Triage a Palisade SAST Report Against the Fernwood Storefront API
7
+ summary: >-
8
+ A fictional SAST tool "Palisade" raised 8 findings against a fictional storefront API and
9
+ declares full-tree coverage at a pinned commit. Most of the report is noise. Classify each
10
+ finding as a true positive or a false positive using only the source and triage context,
11
+ confirm the genuine issues with a reachability chain and a calibrated severity, and refuse to
12
+ inflate. Confirming all 8 is scored as an error, not as diligence.
13
+ prompt: |
14
+ You are the application-security engineer triaging an automated scan. The SAST tool
15
+ "Palisade" ran against the Fernwood storefront API and emitted 8 findings (scan-report).
16
+ Palisade does per-file AST matching: it cannot resolve globally registered guards, cannot
17
+ tell a security hash from a cache key, cannot tell a secret from a publishable identifier,
18
+ and cannot model runtime path-containment checks. Roughly half of what it reported is a
19
+ false positive.
20
+
21
+ You are given the report plus the relevant source and triage context: scan-report,
22
+ order-search, crypto-util, guard-registration, review-render, download-handler, image-proxy,
23
+ and triage-note. Answer each deliverable using ONLY the supplied artifacts. Cite the artifact
24
+ id(s) you rely on for every claim. If something is not defined, not reachable, or not
25
+ exploitable in the artifacts, say so explicitly. This is analysis only: do NOT write exploit
26
+ code, attack payloads, or working bypass strings — the ceiling is explaining the preconditions
27
+ under which an issue is or is not reachable.
28
+
29
+ Over-flagging loses as clearly as missing. A finding is "real" only if you can cite the
30
+ reachable source-to-sink path in the source; a finding is a false positive when another
31
+ artifact shows a citable mechanism that neutralizes it. Do not confirm a finding to appear
32
+ cautious, and do not carry Palisade's severity ratings through uncritically.
33
+
34
+ For findings 1 through 8, give a Verdict of TRUE POSITIVE or FALSE POSITIVE:
35
+ - For a TRUE POSITIVE: give the reachability chain (the source, the sink, and the path
36
+ between them, with artifact-id citations) and a calibrated Severity, and say whether
37
+ Palisade's own rating is accurate or inflated.
38
+ - For a FALSE POSITIVE: name the specific artifact and mechanism that neutralizes the
39
+ finding, and explain why Palisade could not see it.
40
+
41
+ 1. F1 — SQL injection in order-search (rule PAL-SQLI-001). True positive or false positive?
42
+ 2. F2 — hardcoded secret in the payments config (rule PAL-SECRET-014). True or false positive?
43
+ 3. F3 — weak hashing algorithm MD5 in crypto-util (rule PAL-CRYPTO-022). True or false positive?
44
+ 4. F4 — missing authentication on the admin metrics route (rule PAL-AUTHZ-031). True or false?
45
+ 5. F5 — stored XSS in review rendering (rule PAL-XSS-047). True or false positive?
46
+ 6. F6 — path traversal in the invoice download handler (rule PAL-PATH-052). True or false?
47
+ 7. F7 — SSRF in the image proxy (rule PAL-SSRF-060). True or false positive?
48
+ 8. F8 — CSRF on the checkout POST (rule PAL-CSRF-073). True or false positive?
49
+ 9. Give the non-inflated summary: how many of the 8 findings are genuine issues, broken down
50
+ by your calibrated severity (not by Palisade's). This count must reflect only findings you
51
+ confirmed as reachable in deliverables 1–8.
52
+ artifacts:
53
+ - id: scan-report
54
+ type: table
55
+ label: Palisade SAST report for storefront-api (8 findings, declares coverage)
56
+ content: |
57
+ Palisade SAST — storefront-api scan report
58
+ Engine: Palisade 4.6.0 Ruleset: palisade-web-default
59
+ Target: storefront-api @ commit a1b2c3d4 (FULL-TREE scan)
60
+ Coverage: every file under src/ was parsed and analyzed. This report lists EVERY finding
61
+ Palisade raised for this target — nothing is suppressed, deduplicated away, or truncated.
62
+
63
+ Palisade limitations (declared by the vendor): Palisade performs per-file AST analysis. It
64
+ does NOT resolve middleware/guards registered globally at application bootstrap, does NOT
65
+ distinguish a security hash from a non-security checksum, does NOT classify whether a key
66
+ literal is a secret or a publishable identifier, and does NOT evaluate runtime
67
+ path-containment checks.
68
+
69
+ id | rule | palisade_severity | location | finding
70
+ ----|----------------|-------------------|---------------------------------------|-------------------------------------------------------------
71
+ F1 | PAL-SQLI-001 | Critical | src/orders/order-search.ts:12 | user-controlled `sort` concatenated into ORDER BY clause
72
+ F2 | PAL-SECRET-014 | High | src/payments/config.ts:7 | hardcoded secret literal: PUBLISHABLE = 'pub_sandbox_9f2a4c1e...'
73
+ F3 | PAL-CRYPTO-022 | Medium | src/cache/crypto-util.ts:9 | weak hashing algorithm MD5 used to compute a digest
74
+ F4 | PAL-AUTHZ-031 | High | src/admin/metrics.controller.ts:18 | admin route handler declares no in-file authentication decorator
75
+ F5 | PAL-XSS-047 | High | src/reviews/review-render.ts:8 | stored value `r.body` interpolated into HTML without escaping
76
+ F6 | PAL-PATH-052 | High | src/files/download-handler.ts:11 | user-supplied `file` reaches a filesystem read (path traversal)
77
+ F7 | PAL-SSRF-060 | High | src/media/image-proxy.ts:10 | user-supplied `src` URL is fetched server-side (SSRF)
78
+ F8 | PAL-CSRF-073 | Medium | src/orders/checkout.controller.ts:14 | state-changing POST has no anti-CSRF token check
79
+
80
+ Triage instruction: confirm or refute each finding against the source and the triage
81
+ context. A finding Palisade rated high is not automatically real.
82
+ - id: order-search
83
+ type: code
84
+ label: src/orders/order-search.ts — order search query (F1)
85
+ content: |
86
+ // src/orders/order-search.ts
87
+ // Handler for GET /orders/search (authenticated: req.user is set by BearerAuthGuard).
88
+ import { sql } from '../db';
89
+
90
+ export async function searchOrders(req, res) {
91
+ const q = String(req.query.q ?? '');
92
+ const sort = String(req.query.sort ?? 'created_at');
93
+ // `dir` is whitelisted to two literal keywords:
94
+ const dir = req.query.dir === 'asc' ? 'ASC' : 'DESC';
95
+
96
+ // The customer filter and the text filter are parameterized ($1, $2).
97
+ // `sort`, however, is interpolated straight into the ORDER BY clause with
98
+ // no whitelist and no parameterization.
99
+ const rows = await sql.query(
100
+ `SELECT id, total, status, created_at
101
+ FROM orders
102
+ WHERE customer_id = $1
103
+ AND status ILIKE $2
104
+ ORDER BY ${sort} ${dir}
105
+ LIMIT 50`,
106
+ [req.user.customerId, `%${q}%`],
107
+ );
108
+ return res.json(rows);
109
+ }
110
+ - id: crypto-util
111
+ type: code
112
+ label: src/cache/crypto-util.ts — cache-bucket key derivation (F3)
113
+ content: |
114
+ // src/cache/crypto-util.ts
115
+ import { createHash } from 'node:crypto';
116
+
117
+ // Derives a short, stable cache-bucket key from PUBLIC, non-secret inputs
118
+ // (a product id and its last-updated timestamp).
119
+ //
120
+ // This digest is NOT a password hash, NOT a signature, and NOT an integrity
121
+ // check. Nothing about authentication, authorization, or data integrity
122
+ // depends on it. A hash collision here can only route two products to the
123
+ // same cache bucket; the cache layer already stores the full key alongside
124
+ // the value and disambiguates on read, so a collision degrades to a cache
125
+ // miss, never to incorrect data.
126
+ export function cacheBucketKey(productId: string, updatedAt: string): string {
127
+ return createHash('md5').update(`${productId}:${updatedAt}`).digest('hex');
128
+ }
129
+ - id: guard-registration
130
+ type: config
131
+ label: src/app/guards.config — global guard registry (F4)
132
+ content: |
133
+ # src/app/guards.config.ts — application bootstrap guard registration.
134
+ #
135
+ # Palisade analyzes controllers file-by-file and does not resolve guards that
136
+ # are registered globally here, so it reports any handler without an in-file
137
+ # auth decorator as unauthenticated. This file is the authoritative guard
138
+ # registry: every request is matched against it BEFORE reaching any handler.
139
+
140
+ globalGuards:
141
+ - BearerAuthGuard # applied to every route in the application
142
+ - AdminScopeGuard # additionally requires scope=admin on any path under /admin
143
+
144
+ routePrefixGuards:
145
+ # Exhaustive path-prefix guard table.
146
+ - prefix: /admin
147
+ guards: [BearerAuthGuard, AdminScopeGuard] # covers /admin/metrics AND all other /admin routes
148
+ - prefix: /orders
149
+ guards: [BearerAuthGuard]
150
+ - prefix: /media
151
+ guards: [BearerAuthGuard]
152
+ - prefix: /files
153
+ guards: [BearerAuthGuard]
154
+
155
+ # There are no unguarded route prefixes. The metrics controller at
156
+ # src/admin/metrics.controller.ts resolves under /admin and is therefore
157
+ # covered by BearerAuthGuard + AdminScopeGuard even though it declares no
158
+ # in-file decorator. This table is the complete authentication surface.
159
+ - id: review-render
160
+ type: code
161
+ label: src/reviews/review-render.ts — product review rendering (F5)
162
+ content: |
163
+ // src/reviews/review-render.ts
164
+ // Renders the stored product reviews into server-side HTML for the public
165
+ // product page. `reviews` are submitted by authenticated customers via
166
+ // POST /products/:id/reviews and stored verbatim; every shopper who views
167
+ // the product page — and the staff who view the moderation queue — receives
168
+ // this HTML.
169
+ import { escapeHtml } from '../util/escape';
170
+
171
+ export function renderReviews(reviews: Review[]): string {
172
+ return reviews
173
+ .map(
174
+ (r) => `
175
+ <li class="review">
176
+ <span class="review-author">${escapeHtml(r.author)}</span>
177
+ <div class="review-body">${r.body}</div>
178
+ </li>`,
179
+ )
180
+ .join('');
181
+ }
182
+ // Note: r.author is passed through escapeHtml; r.body is interpolated raw.
183
+ - id: download-handler
184
+ type: code
185
+ label: src/files/download-handler.ts — invoice download (F6)
186
+ content: |
187
+ // src/files/download-handler.ts
188
+ import { resolve, join, relative, isAbsolute } from 'node:path';
189
+ import { createReadStream } from 'node:fs';
190
+
191
+ const ROOT = resolve('/srv/storefront/invoices');
192
+
193
+ // Handler for GET /files/invoice?file=... This is the ONLY filesystem read
194
+ // path in the service; no other handler opens files from user input.
195
+ export function downloadInvoice(req, res) {
196
+ const requested = String(req.query.file ?? '');
197
+
198
+ // Resolve the candidate against ROOT, then verify it did not escape ROOT.
199
+ const candidate = resolve(join(ROOT, requested));
200
+ const rel = relative(ROOT, candidate);
201
+ if (rel.startsWith('..') || isAbsolute(rel)) {
202
+ // Any traversal (../.., an absolute path, etc.) resolves to a location
203
+ // outside ROOT, making `rel` start with '..' or be absolute, and is
204
+ // rejected here BEFORE any filesystem access happens.
205
+ return res.status(400).json({ error: 'invalid path' });
206
+ }
207
+
208
+ return createReadStream(candidate).pipe(res);
209
+ }
210
+ - id: image-proxy
211
+ type: code
212
+ label: src/media/image-proxy.ts — remote image proxy (F7)
213
+ content: |
214
+ // src/media/image-proxy.ts
215
+ import { fetch } from '../http';
216
+
217
+ // Handler for GET /media/proxy?src=<url>. Fetches a remote image server-side
218
+ // so the browser can display third-party images without mixed-content
219
+ // warnings. Reachable by any authenticated caller (BearerAuthGuard on /media).
220
+ export async function proxyImage(req, res) {
221
+ const src = String(req.query.src ?? '');
222
+
223
+ // `src` is taken as-is from the query string. There is no host allow-list,
224
+ // no restriction to known image CDNs, and no check against private or
225
+ // link-local address ranges. The server issues the request and pipes the
226
+ // upstream response body straight back to the caller.
227
+ const upstream = await fetch(src);
228
+ res.setHeader('content-type', upstream.headers.get('content-type') ?? 'application/octet-stream');
229
+ return upstream.body.pipe(res);
230
+ }
231
+ - id: triage-note
232
+ type: note
233
+ label: triage-note — storefront platform team (authoritative for classification)
234
+ content: |
235
+ Triage context for the Palisade scan. Authored by the storefront platform team;
236
+ authoritative for classifying the findings below.
237
+
238
+ Authentication model — the storefront API is STATELESS and BEARER-ONLY. Every request
239
+ must carry `Authorization: Bearer <token>`. The server keeps no session state and sets
240
+ no authentication cookies. Because no credential is stored ambiently in the browser, a
241
+ browser never attaches credentials automatically to a cross-site request, so cross-site
242
+ request forgery cannot be mounted against any endpoint here. There is no cookie-based or
243
+ session-based authentication anywhere in this service — this is the complete auth model.
244
+
245
+ Payment keys — the literal Palisade flagged as F2 in src/payments/config.ts is
246
+ `pub_sandbox_9f2a4c1e...`, our payment provider's PUBLISHABLE sandbox key. The
247
+ `pub_sandbox_` prefix denotes a publishable key: it is designed to be shipped inside
248
+ client-side code, it only identifies the merchant to the provider, and it carries NO
249
+ secret authority — it cannot charge cards, issue refunds, or read account data. The
250
+ secret key (prefix `sk_live_`) is loaded only from the STOREFRONT_PAYMENT_SECRET
251
+ environment variable and never appears in source. F2 is therefore a publishable
252
+ identifier, not a leaked secret.
253
+
254
+ Scope of this note: it speaks ONLY to the CSRF finding (F8) and the payment-key finding
255
+ (F2). It does not excuse any other finding — judge F1, F3, F4, F5, F6, and F7 on their
256
+ own source.
257
+ tags: [security, finding-triage, sast, false-positives, triage]
@@ -0,0 +1,165 @@
1
+ id: vuln-path-sanitizer-escape
2
+ version: 1.0.0
3
+ category: security
4
+ cluster: vuln-discovery
5
+ difficulty: expert
6
+ title: Portage Artifact Server Path Sanitizer Review
7
+ summary: Review a fictional artifact file server whose path sanitizer strips traversal with a single-pass string replace after URL-decoding. One real containment flaw hides among benign look-alikes (claimed missing auth, symlink escape, wildcard CORS, a nearby SQL lookup) that are each neutralized elsewhere.
8
+ prompt: |
9
+ You are performing a defensive security review of "Portage", a fictional
10
+ artifact file server that streams stored objects to authenticated callers. Six
11
+ artifacts describe the request path: serve-handler, path-util, auth-middleware,
12
+ storage-config, metadata-query, and service-note.
13
+
14
+ Exactly one of the behaviors in these artifacts is a real containment flaw; the
15
+ others are look-alikes that are neutralized somewhere in the same artifacts.
16
+ Answer each deliverable using ONLY the supplied artifacts. Cite the artifact
17
+ id(s) you rely on for every claim. If something is not defined or not reachable
18
+ in the artifacts, say so explicitly. Reason about the flaw ONLY — do NOT provide
19
+ an exploit path, traversal string, or any working payload.
20
+
21
+ 1. Identify the flaw in the artifact read path and quote the exact expression
22
+ that is responsible for it. Name the vulnerability class in standard
23
+ terminology.
24
+
25
+ 2. Explain WHY the sanitizer cannot guarantee that the file read stays within
26
+ the storage root. Reason about the property of the sanitizing step and the
27
+ order of operations in serve-handler — not about any specific input string.
28
+
29
+ 3. A reviewer flags the endpoint as having no authentication. Is that the flaw?
30
+ Rule it in or out and cite the evidence.
31
+
32
+ 4. A reviewer flags a symlink-based escape out of the storage root. Is that
33
+ reachable? Rule it in or out and cite the mechanism that decides it.
34
+
35
+ 5. A reviewer flags the wildcard CORS header as a vulnerability. Is it? Rule it
36
+ in or out and cite the evidence.
37
+
38
+ 6. Give the minimal fix that actually guarantees containment. State it precisely
39
+ in terms of what must be computed and asserted before the file is read.
40
+ artifacts:
41
+ - id: serve-handler
42
+ type: code
43
+ label: Portage artifact download handler
44
+ content: |
45
+ // serve-handler.ts — Portage artifact download endpoint.
46
+ // Mounted at GET /artifacts/* (router wiring in service-note).
47
+
48
+ import { join } from 'node:path';
49
+ import { createReadStream } from 'node:fs';
50
+ import { sanitizeRelPath } from './path-util';
51
+ import { storage } from './storage-config';
52
+ import { lookupArtifactMeta } from './metadata-query';
53
+
54
+ export async function serveArtifact(req: Req, res: Res): Promise<void> {
55
+ // req.params.path is everything after /artifacts/ , e.g. "reports/q3.pdf".
56
+ const requested = decodeURIComponent(req.params.path); // (1) URL-decode first
57
+ const safeRel = sanitizeRelPath(requested); // (2) strip traversal
58
+
59
+ // Metadata lookup by the ORIGINAL key (parameterized — see metadata-query).
60
+ const meta = await lookupArtifactMeta(req.params.path);
61
+
62
+ // (3) join the sanitized relative path to the root, then stream it. There
63
+ // is no post-join canonicalization or containment check here.
64
+ const absPath = join(storage.root, safeRel);
65
+ res.setHeader('Content-Type', meta?.contentType ?? 'application/octet-stream');
66
+ createReadStream(absPath).pipe(res);
67
+ }
68
+ - id: path-util
69
+ type: code
70
+ label: Portage path sanitizer helper
71
+ content: |
72
+ // path-util.ts — Portage path helpers.
73
+
74
+ // Removes parent-directory traversal from a relative path before it is joined
75
+ // to the storage root.
76
+ //
77
+ // NOTE: this is a single textual pass. It does not canonicalize the result and
78
+ // it does not re-scan text it has already rewritten.
79
+ export function sanitizeRelPath(p: string): string {
80
+ // drop any leading slash so join() treats the value as relative
81
+ const rel = p.replace(/^\/+/, '');
82
+ // remove "../" sequences
83
+ return rel.replace(/\.\.\//g, '');
84
+ }
85
+ - id: auth-middleware
86
+ type: code
87
+ label: Portage request authentication middleware
88
+ content: |
89
+ // auth-middleware.ts — Portage request authentication.
90
+ // Applied to every /artifacts route (see service-note router wiring).
91
+
92
+ export function requireAuth(req: Req, res: Res, next: Next): void {
93
+ const token = req.headers['x-portage-token'];
94
+ const session = verifySession(token); // null if the token is missing/invalid
95
+ if (!session) {
96
+ res.status(401).end();
97
+ return;
98
+ }
99
+ req.session = session;
100
+ next();
101
+ }
102
+ - id: storage-config
103
+ type: config
104
+ label: Portage object storage configuration (production)
105
+ content: |
106
+ # portage.storage.yaml — storage backend (production), full block, no keys omitted.
107
+ root: /var/portage/objects
108
+ backend: flat-object-store # every artifact is one opaque object; no nested real dirs
109
+
110
+ symlinks:
111
+ enabled: false # the store rejects symlink objects; none can exist in the tree
112
+ followOnRead: false # reads never traverse a symlink even if one somehow appeared
113
+
114
+ maxObjectBytes: 52428800
115
+
116
+ # This is the complete storage configuration. The store is a flat object
117
+ # bucket: there are no symbolic links, hard links, or mount points inside
118
+ # root. A symlink-based escape is therefore not possible against this backend.
119
+ - id: metadata-query
120
+ type: code
121
+ label: Portage artifact metadata lookup
122
+ content: |
123
+ // metadata-query.ts — artifact metadata lookup.
124
+ import { db } from './db';
125
+
126
+ export async function lookupArtifactMeta(key: string): Promise<ArtifactMeta | null> {
127
+ // Parameterized query: `key` is BOUND as $1, never interpolated into the SQL
128
+ // text. No string concatenation is used to build this statement.
129
+ const rows = await db.query(
130
+ 'SELECT content_type, byte_size FROM artifact_meta WHERE object_key = $1 LIMIT 1',
131
+ [key],
132
+ );
133
+ return rows[0]
134
+ ? { contentType: rows[0].content_type, byteSize: rows[0].byte_size }
135
+ : null;
136
+ }
137
+ - id: service-note
138
+ type: note
139
+ label: Portage service notes (authoritative, as-deployed)
140
+ content: |
141
+ Portage service notes — authoritative; matches the deployed router.
142
+
143
+ Router wiring (order matters):
144
+ router.use(requireAuth); // EVERY /artifacts route is behind auth
145
+ router.get('/artifacts/*', serveArtifact);
146
+ There is no unauthenticated route to serveArtifact; requireAuth runs first on
147
+ every request that reaches it (auth-middleware). A valid X-Portage-Token is
148
+ required, but the token places NO constraint on which relative path a caller
149
+ may request.
150
+
151
+ CORS: the /artifacts API sends `Access-Control-Allow-Origin: *` but never sets
152
+ `Access-Control-Allow-Credentials`. Responses carry no cookies and no ambient
153
+ auth; the only credential is the explicit X-Portage-Token header, which a
154
+ browser will not attach cross-origin under a wildcard / no-credentials policy.
155
+ The wildcard is intentional so partners can read artifacts they already hold a
156
+ token for; it exposes nothing a caller could not already fetch with that token.
157
+
158
+ Storage: the backend is a flat object store with symlinks disabled
159
+ (storage-config). The only path-derived value that reaches the filesystem is
160
+ the result of sanitizeRelPath joined to storage.root inside serve-handler.
161
+
162
+ This note, the two code files, path-util, storage-config, and metadata-query
163
+ are the COMPLETE request path for artifact reads. There is no other handler,
164
+ sanitizer, or filesystem read for artifacts.
165
+ tags: [security, vuln-discovery, path-traversal, input-sanitization, containment]