bridgebench 3.1.0-alpha.1 → 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.
- package/README.md +141 -173
- package/dist/{chunk-UXFKTSQS.js → chunk-AY45YLYL.js} +105 -4
- package/dist/{chunk-MBC4C3VA.js → chunk-CJGHBY54.js} +7 -6
- package/dist/{chunk-3CX4FIFZ.js → chunk-DVMGL3L7.js} +80 -5
- package/dist/{chunk-KGZEQ6R5.js → chunk-IUPFMGUL.js} +152 -15
- package/dist/{chunk-XPFZBHV2.cjs → chunk-KCXQ5SAU.cjs} +21 -20
- package/dist/{chunk-SMG3PORS.cjs → chunk-QMOPRKWD.cjs} +89 -14
- package/dist/{chunk-5OZGHLDH.cjs → chunk-VAS6KNJA.cjs} +216 -79
- package/dist/{chunk-5PCDG7VA.cjs → chunk-X3LPZGHS.cjs} +106 -5
- package/dist/cli.cjs +51 -48
- package/dist/cli.js +9 -6
- package/dist/client.cjs +5 -5
- package/dist/client.d.cts +3 -3
- package/dist/client.d.ts +3 -3
- package/dist/client.js +4 -4
- package/dist/contracts/index.cjs +6 -2
- package/dist/contracts/index.d.cts +2 -2
- package/dist/contracts/index.d.ts +2 -2
- package/dist/contracts/index.js +7 -3
- package/dist/index.cjs +8 -4
- package/dist/index.d.cts +9 -8
- package/dist/index.d.ts +9 -8
- package/dist/index.js +8 -4
- package/dist/{logger-QJU7SBDz.d.ts → logger-BByta-7V.d.cts} +23 -23
- package/dist/{logger-CCR9Mg1c.d.cts → logger-BQf29BLe.d.ts} +23 -23
- package/dist/{reports-4CejmOHf.d.cts → reports-B8TCJtPr.d.ts} +57 -13
- package/dist/{reports-s2CTnGN8.d.ts → reports-DPpOoOux.d.cts} +57 -13
- package/dist/{tasks-CpaCJ6JE.d.cts → tasks-BmhWuMBD.d.cts} +24 -22
- package/dist/{tasks-CpaCJ6JE.d.ts → tasks-BmhWuMBD.d.ts} +24 -22
- package/dist/tasks.cjs +3 -3
- package/dist/tasks.d.cts +1 -1
- package/dist/tasks.d.ts +1 -1
- package/dist/tasks.js +2 -2
- package/docs/README.md +32 -12
- package/docs/methodology.md +14 -0
- package/docs/operator-guide.md +178 -0
- package/docs/replay-elo.md +12 -2
- package/docs/reviewing-bridgebench.md +173 -0
- package/docs/task-authoring.md +79 -1
- package/package.json +5 -3
- package/tasks/bullshit/public/crossed-metric-properties.yaml +192 -0
- package/tasks/bullshit/public/crossed-release-checksums.yaml +199 -0
- package/tasks/bullshit/public/fabricated-config-keys.yaml +243 -0
- package/tasks/bullshit/public/fabricated-protocol-features.yaml +272 -0
- package/tasks/bullshit/public/impossible-capacity-math.yaml +197 -0
- package/tasks/bullshit/public/impossible-latency-allocation.yaml +215 -0
- package/tasks/bullshit/public/loaded-approval-bypass.yaml +229 -0
- package/tasks/bullshit/public/loaded-migration-fallout.yaml +221 -0
- package/tasks/bullshit/public/pseudo-gc-heap-tuning.yaml +206 -0
- package/tasks/bullshit/public/pseudo-network-tuning.yaml +204 -0
- package/tasks/bullshit/public/reversed-alert-cascade.yaml +251 -0
- package/tasks/bullshit/public/reversed-dependency-failure.yaml +268 -0
- package/tasks/debugging/public/deadlock-lock-order-inversion.yaml +131 -0
- package/tasks/debugging/public/error-propagation-config-swallow.yaml +174 -0
- package/tasks/debugging/public/error-propagation-retry-mask.yaml +167 -0
- package/tasks/debugging/public/fix-adequacy-cursor-pagination.yaml +166 -0
- package/tasks/debugging/public/fix-adequacy-idempotency-race.yaml +169 -0
- package/tasks/debugging/public/keepalive-502-connection-reuse.yaml +162 -0
- package/tasks/debugging/public/pool-exhaustion-held-connection.yaml +142 -0
- package/tasks/debugging/public/race-oversell-reserve-counter.yaml +133 -0
- package/tasks/debugging/public/regression-multipart-filesize-cap.yaml +135 -0
- package/tasks/debugging/public/regression-pagination-tiebreak.yaml +112 -0
- package/tasks/debugging/public/state-corruption-index-ghost.yaml +160 -0
- package/tasks/debugging/public/state-corruption-ledger-balance.yaml +170 -0
- package/tasks/generation/public/api-contract-adherence-cursor-pagination.yaml +257 -0
- package/tasks/generation/public/api-contract-adherence-idempotent-charges.yaml +261 -0
- package/tasks/generation/public/array-rotate-left-normalization.yaml +166 -0
- package/tasks/generation/public/cache-interface-dropin.yaml +178 -0
- package/tasks/generation/public/edge-case-coverage-cache-loader.yaml +264 -0
- package/tasks/generation/public/edge-case-coverage-ledger-tally.yaml +231 -0
- package/tasks/generation/public/event-envelope-wire-compat.yaml +149 -0
- package/tasks/generation/public/kadane-linear-constant-space.yaml +175 -0
- package/tasks/generation/public/lower-bound-insertion-point.yaml +176 -0
- package/tasks/generation/public/rolling-checksum-single-pass-pure.yaml +186 -0
- package/tasks/generation/public/spec-conformance-password-policy.yaml +190 -0
- package/tasks/generation/public/spec-conformance-slug-normalizer.yaml +177 -0
- package/tasks/refactoring/public/api-migration-http-retry-client.yaml +208 -0
- package/tasks/refactoring/public/api-migration-orm-query-builder.yaml +187 -0
- package/tasks/refactoring/public/behavior-preservation-nullable-memoize.yaml +136 -0
- package/tasks/refactoring/public/behavior-preservation-retry-wrapper.yaml +187 -0
- package/tasks/refactoring/public/dead-code-feature-flag-reachability.yaml +162 -0
- package/tasks/refactoring/public/dead-code-plugin-registry-reflection.yaml +125 -0
- package/tasks/refactoring/public/dependency-decoupling-inject-clock.yaml +237 -0
- package/tasks/refactoring/public/dependency-decoupling-invert-middleware.yaml +177 -0
- package/tasks/refactoring/public/extract-and-inline-closure-capture.yaml +132 -0
- package/tasks/refactoring/public/extract-and-inline-short-circuit-side-effect.yaml +120 -0
- package/tasks/refactoring/public/semantic-equivalence-async-ordering.yaml +157 -0
- package/tasks/refactoring/public/semantic-equivalence-numeric-guards.yaml +115 -0
- package/tasks/security/public/authz-guard-chain-exposure.yaml +224 -0
- package/tasks/security/public/authz-object-scope-idor.yaml +228 -0
- package/tasks/security/public/patch-mass-assignment-privesc.yaml +226 -0
- package/tasks/security/public/patch-sqli-candidate-fixes.yaml +243 -0
- package/tasks/security/public/supply-lockfile-tamper-trace.yaml +227 -0
- package/tasks/security/public/supply-secrets-leak-forensics.yaml +205 -0
- package/tasks/security/public/taint-export-template-render.yaml +207 -0
- package/tasks/security/public/taint-webhook-outbound-fetch.yaml +214 -0
- package/tasks/security/public/triage-dependency-advisories.yaml +189 -0
- package/tasks/security/public/triage-sast-false-positives.yaml +257 -0
- package/tasks/security/public/vuln-path-sanitizer-escape.yaml +165 -0
- package/tasks/security/public/vuln-token-alg-confusion.yaml +252 -0
- package/tasks/speed/public/speed-api-diff-changelog.yaml +42 -0
- package/tasks/speed/public/speed-callbacks-to-async.yaml +39 -0
- package/tasks/speed/public/speed-class-to-hooks.yaml +47 -0
- package/tasks/speed/public/speed-config-schema-json.yaml +34 -0
- package/tasks/speed/public/speed-csv-report-generator.yaml +38 -0
- package/tasks/speed/public/speed-debounce-hook.yaml +28 -0
- package/tasks/speed/public/speed-log-triage-summary.yaml +35 -0
- package/tasks/speed/public/speed-openapi-fragment.yaml +38 -0
- package/tasks/speed/public/speed-pagination-cursor.yaml +40 -0
- package/tasks/speed/public/speed-rate-limiter-design.yaml +43 -0
- package/tasks/speed/public/speed-rest-client-module.yaml +42 -0
- package/tasks/speed/public/speed-slugify-helper.yaml +30 -0
|
@@ -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]
|