Skip to content

Commit 28b64a8

Browse files
authored
chore: improvements to issues and PR triagers (#16143)
1 parent 150709e commit 28b64a8

6 files changed

Lines changed: 313 additions & 22 deletions

File tree

.claude/skills/reviewing-prs/SKILL.md

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ ignored.
4141
- **Checking contribution guidelines?** → MUST load `reference/contribution-types.md` first
4242
- **Verifying code conventions?** → MUST load `reference/conventions.md` first
4343
- **Reviewing a dependency-update PR (Dependabot / Renovate / lockfile bump)?** → MUST load `reference/dependency-review.md` first
44+
- **Running the security analysis (Step 10)?** → MUST load `reference/security-review.md` first (trust-boundary heuristic + Medusa-specific patterns)
4445
- **Writing the review summary / blocking points?** → MUST load `reference/comment-guidelines.md` first (includes bug, security, and performance reporting formats)
4546

4647
**Minimum requirement:** Load at least the relevant reference file(s) before completing the review.
@@ -325,22 +326,82 @@ Each such comment is a **required change**: emit
325326

326327
> **CRITICAL:** Applies to **all PRs**, including team members. Read the actual diff; before flagging, read the full file. Only flag issues in added (`+`) lines.
327328
329+
> **MUST load `reference/security-review.md` before this step.** It explains
330+
> the trust-boundary / taint-tracing method and Medusa-specific patterns
331+
> (object-storage key traversal, DB/query-filter injection, unescaped
332+
> JSON/HTML output, the "widened input" red flag). The checklist below is a
333+
> reminder, not a substitute.
334+
335+
**How to look, not just what to look for:** for the changed code, trace
336+
**tainted input** (request bodies/params/headers, uploaded file names and
337+
contents, webhook payloads, and any entity field set from them) to a
338+
**sensitive sink** (path/key construction, URL fetch, SQL/query filter, shell,
339+
`eval`, response, log). A finding is: tainted value reaches a sink without
340+
validation in between. Read callers/types when you can't tell if a value is
341+
tainted — a "filename" or "key" is frequently set straight from an upload
342+
request.
343+
344+
**Highest-value red flag — a diff that WIDENS what user input reaches a sink.**
345+
The most-missed security bug is not new dangerous code but the *removal of an
346+
implicit protection*: code that used to use only a sanitized fragment of an
347+
input now uses more of it (e.g. it kept only a filename's base name and now
348+
also prepends the parsed **directory**), or a `basename`/allow-list/regex/cap/
349+
`encodeURIComponent` is dropped, or a fixed value becomes request-configurable.
350+
When the diff routes more of an input into a path/key/URL/query, ask *"what is
351+
the worst string an attacker can put here, and where does it end up?"*
352+
328353
Check for:
329354

330355
**Authentication & Authorization:**
331356
- Missing or bypassed authentication middleware on new routes
332357
- Authorization checks missing — any route that accesses or mutates data scoped to a user/store must verify ownership
333358
- Privilege escalation
334359

335-
**Injection & Execution:**
336-
- Raw SQL constructed from user input (SQL injection)
360+
**Database injection (not just raw SQL):**
361+
- Raw SQL / MikroORM / Knex from user input — string-interpolated `em.execute()`,
362+
`knex.raw()`, `.raw()` fragments instead of bound parameters
363+
- **Query-filter / operator injection**`req.body` / `req.query` /
364+
`req.filterableFields` passed straight into a service `.list*()`, repository,
365+
or `query.graph({ filters })` without a validator, letting a caller inject
366+
operators (`$ne`, `$or`, `$like`, …) or filter on unintended columns to read
367+
or bypass scoped data. Routes must validate/whitelist the request (Zod /
368+
`validateAndTransformBody`) and pass only known fields into the filter.
369+
- Dynamic column / order / table names from user input without an allow-list
370+
371+
**Other injection & execution:**
337372
- `eval()`, `new Function()`, `vm.runInContext()` with untrusted data
338373
- Dynamic `require()`/`import()` with user-controlled paths
339374
- Shell command construction with user input
340375

341-
**Input Validation:**
342-
- User-controlled input to filesystem operations without sanitization → path traversal
343-
- Missing size/length limits → DoS
376+
**Output encoding — unescaped JSON / HTML (commonly missed):**
377+
- **Unescaped JSON in an HTML/`<script>` context (XSS)** — interpolating
378+
`JSON.stringify(data)` into an HTML string or inline script. `JSON.stringify`
379+
does NOT escape HTML, so a value with `</script>` (or `<!--`, U+2028/U+2029)
380+
breaks out and injects markup. Escape `<`/`>`/`&`/line separators, or use a
381+
`data-*`/DOM API instead of string concatenation.
382+
- User input reflected into any HTML/markup response (pages, emails, invoices,
383+
SVGs, redirect params) without escaping → XSS/HTML injection
384+
- Hand-built JSON via string concatenation instead of `JSON.stringify`
385+
- `JSON.parse` on untrusted input without try/catch; parsed objects merged via
386+
`Object.assign`/spread/deep-merge without guarding `__proto__`
387+
prototype pollution
388+
- Returning user-controlled text as `text/html` (or a sniffable missing
389+
`Content-Type`) when it should be `application/json`/`text/plain`
390+
391+
**Path / key traversal (NOT just `fs.*`):**
392+
- User-controlled input built into a **filesystem path** without sanitization
393+
- User-controlled input built into an **object-storage key / bucket path**
394+
(S3/GCS/R2 `Key`, `Upload`, presigned URLs) — cloud SDKs treat the key as an
395+
opaque string, so `..` or a leading `/` in a filename can **escape a
396+
configured prefix and cross a tenant/namespace boundary or overwrite another
397+
object.** Prefixing a string does NOT stop `..` from climbing out of it.
398+
- `..` / leading `/` (and encoded forms `%2e%2e`, `%2f`) reaching a cache key,
399+
URL path, redirect target, or archive entry name (zip-slip)
400+
- Fix expectation: strip/reject `..` and leading `/` (or derive the safe part
401+
via `path.basename`/an allow-list) **before** building the path/key
402+
403+
**Other input validation:**
404+
- Missing size/length/pagination limits → DoS
344405
- Unvalidated external URLs in server-side fetches → SSRF
345406

346407
**Data Exposure:**
@@ -479,6 +540,13 @@ the workflow event — never from JSON-supplied numbers.
479540
- [ ] Skipping the integration test check for API route changes in `packages/medusa/src/api/`
480541
- [ ] Not fetching PR details when they weren't passed as arguments
481542
- [ ] Skipping security analysis for team member PRs — security analysis applies to ALL PRs
543+
- [ ] Running Step 10 without loading `reference/security-review.md`
544+
- [ ] Treating path traversal as a filesystem-only issue — object-storage keys, cache keys, and URL paths are equally vulnerable to `..` / leading `/`
545+
- [ ] Missing a change that widens what user input reaches a path/key/URL (e.g. a filename's directory now prepended to a storage key) — trace the tainted value to its sink
546+
- [ ] Assuming a configured prefix/base dir contains the final path — it does not stop `..` from climbing out
547+
- [ ] Treating DB injection as raw-SQL-only — unvalidated `req.body`/`req.query` passed into a service `.list*()`/repository/`query.graph({ filters })` allows operator injection and reading unscoped data
548+
- [ ] Missing unescaped JSON/HTML — `JSON.stringify(userData)` interpolated into an HTML/`<script>` context is XSS; user input reflected into any markup response must be escaped
549+
- [ ] Overlooking prototype pollution — a parsed JSON body merged via `Object.assign`/spread/deep-merge without guarding `__proto__`
482550
- [ ] Skipping performance analysis — always check for N+1 queries and unbounded queries
483551
- [ ] Setting `review_template: "approve"` while listing a confirmed security or blocking performance issue
484552
- [ ] Flagging style/code smell as bugs
@@ -502,5 +570,6 @@ the workflow event — never from JSON-supplied numbers.
502570
reference/conventions.md - Medusa coding conventions to verify
503571
reference/contribution-types.md - How to verify code, docs, and admin translation contributions
504572
reference/dependency-review.md - How to review dependency-update PRs (release notes, breaking changes, Medusa usage, test areas)
573+
reference/security-review.md - Trust-boundary/taint method + Medusa security patterns (path/key traversal, DB/filter injection, unescaped JSON/HTML); load before Step 10
505574
reference/comment-guidelines.md - Tone and phrasing rules; use as guidance for `summary` and `blocking_points`
506575
```

.claude/skills/reviewing-prs/reference/comment-guidelines.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ Keep concerns concise and factual — describe the problem, not a lecture. If un
103103
104104
## Potential Bugs Section
105105
106-
When bugs are found in Step 10, include a **"Potential Bugs"** section in the review comment. **All bugs — confirmed or suspected — are required changes.** Always apply `requires-more` when this section is present.
106+
When bugs are found in Step 12, include a **"Potential Bugs"** section in the review comment. **All bugs — confirmed or suspected — are required changes.** Always apply `requires-more` when this section is present.
107107
108108
**Format:**
109109
@@ -129,7 +129,7 @@ When bugs are found in Step 10, include a **"Potential Bugs"** section in the re
129129
130130
## Security Issues Section
131131
132-
When security issues are found in Step 8, include a **"Security Issues"** section in the review comment.
132+
When security issues are found in Step 10, include a **"Security Issues"** section in the review comment.
133133
134134
- Confirmed security vulnerabilities → add to **Required changes** and always apply `requires-more`
135135
- Suspected / uncertain risks → include under **"Security Issues"** with a question framing
@@ -158,7 +158,7 @@ When security issues are found in Step 8, include a **"Security Issues"** sectio
158158
159159
## Performance Issues Section
160160
161-
When performance issues are found in Step 9, include a **"Performance Issues"** section in the review comment.
161+
When performance issues are found in Step 11, include a **"Performance Issues"** section in the review comment.
162162
163163
- Blocking performance issues (N+1 queries, unbounded queries on large tables, missing pagination) → add to **Required changes** and apply `requires-more`
164164
- Non-blocking performance observations → include under **"Performance Issues"** as notes only
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# Security Review Reference
2+
3+
Load this before Step 10. The inline checklist in `SKILL.md` names the
4+
vulnerability classes; this file gives you the **mental model** for finding
5+
them and Medusa-specific patterns that are easy to miss.
6+
7+
## The core heuristic: trace tainted input to a dangerous sink
8+
9+
Most real vulnerabilities are the same shape: **user-controlled input reaches
10+
a sensitive operation without validation in between.** Don't scan for scary
11+
function names — trace data flow.
12+
13+
1. **Identify the sources** the diff introduces or touches. Anything an
14+
external caller can influence is tainted: request bodies/params/query,
15+
headers, uploaded file names and contents, webhook payloads, entity fields
16+
that were set from any of the above, and values read back from the DB that
17+
originally came from a user.
18+
2. **Identify the sinks** in the changed code: filesystem paths, **object
19+
storage keys / bucket paths**, URLs used in server-side fetches, SQL / query
20+
filters, shell commands, `eval`/`Function`, dynamic `require`/`import`,
21+
redirect targets, template rendering, response bodies, and logs.
22+
3. **Check what happens between source and sink.** Is the input validated,
23+
allow-listed, normalized, length-capped, or escaped? If a tainted value
24+
flows into a sink untouched, that is your finding — regardless of how
25+
"normal" the surrounding code looks.
26+
27+
If you cannot tell from the diff whether a value is tainted, **read the
28+
callers and the type definition** to see where it originates. A filename or key
29+
that looks internal is often set directly from an upload request.
30+
31+
## Red flag: a diff that WIDENS what user input reaches a sink
32+
33+
The most commonly missed security bug is not new dangerous code — it is a
34+
change that **removes an implicit protection**. Watch for:
35+
36+
- Code that previously used only a *sanitized fragment* of an input and now
37+
uses **more** of it. Example: parsing a filename and previously using only
38+
the base name + extension, then changing it to also prepend the parsed
39+
**directory** — that directory is attacker-controlled and can contain `..`
40+
or leading `/`.
41+
- Removing a `replace(/[^a-z0-9]/…)`, a `path.basename()`, an allow-list
42+
check, a length cap, or an `encodeURIComponent`.
43+
- Loosening a validator/regex, widening an accepted type, or making a
44+
previously-fixed value configurable from the request.
45+
- Concatenating a new user-controlled segment into an existing path, key, URL,
46+
or identifier string.
47+
48+
When you see the diff route more of an input into a path/key/URL/query, stop
49+
and ask: *"What is the worst string an attacker can put here, and where does it
50+
end up?"* Then confirm the code rejects or neutralizes that string.
51+
52+
## Path / key traversal — filesystem AND object storage
53+
54+
Path traversal is **not limited to `fs.*` calls.** Any construction of a path,
55+
key, or identifier from user input is in scope:
56+
57+
- **Filesystem:** `fs.readFile`, `fs.writeFile`, `createReadStream`,
58+
`path.join(base, userInput)`, static file serving.
59+
- **Object storage keys** (S3, GCS, R2, etc.): the `Key` / object path passed
60+
to `PutObject`, `GetObject`, `Upload`, presigned-URL helpers. Cloud SDKs
61+
treat the key as an opaque string and will happily store `a/../../b`, so
62+
`..` and leading `/` segments let a caller **escape a configured prefix,
63+
cross a tenant/namespace boundary, or overwrite another object.**
64+
- **Cache keys, URL paths, redirect targets, archive entry names** (zip-slip).
65+
66+
What to require in the fix:
67+
- Reject or strip `..` segments and leading `/` (and their URL-encoded forms
68+
`%2e%2e`, `%2f`) **before** the value is used to build the path/key.
69+
- Prefer deriving the safe part explicitly (e.g. `path.basename`, an
70+
allow-list of characters) over trying to blacklist bad sequences.
71+
- Confirm a configured prefix / base directory actually **contains** the final
72+
resolved path — prefixing a string does not prevent `..` from climbing out
73+
of it.
74+
75+
Concrete pattern that MUST be flagged (this is the shape of a real miss):
76+
77+
```ts
78+
// filename comes from the upload request (tainted)
79+
const parsed = path.parse(file.filename)
80+
const dir = parsed.dir ? `${parsed.dir}/` : ""
81+
// prefix is meant to isolate this tenant's files
82+
const key = `${config.prefix}${dir}${parsed.name}-${ulid()}${parsed.ext}`
83+
// filename "../../other-tenant/logo.png" -> key climbs out of `prefix`
84+
```
85+
86+
Finding: *path traversal in the storage key — a crafted `filename` containing
87+
`..` escapes `config.prefix` and can write outside the intended
88+
tenant/namespace. Fix: strip `..`/leading-`/` segments (or reject them) before
89+
building the key.*
90+
91+
## Database injection — beyond raw SQL string concatenation
92+
93+
Raw SQL built from user input (`` `... WHERE id = '${req.params.id}'` ``) is the
94+
obvious case, but Medusa uses MikroORM and the remote-query layer, which have
95+
their own injection surfaces. Flag any of these when the value is tainted:
96+
97+
- **Raw MikroORM / Knex queries:** `em.execute(...)`, `knex.raw(...)`,
98+
`manager.getConnection().execute(...)`, or `.raw(...)` fragments built by
99+
string interpolation instead of bound parameters (`?` / named bindings).
100+
- **Query-filter / operator injection:** passing `req.body`, `req.query`, or
101+
`req.filterableFields` **straight into** a service `.list*()`, repository, or
102+
`query.graph({ filters })` call without a validator. An attacker can inject
103+
operators (`$ne`, `$like`, `$or`, `$gt`, …) or filter on unintended columns
104+
to read rows they should not see, bypass a scope filter, or turn an equality
105+
check into a broad match. Medusa routes are expected to validate/whitelist
106+
the request schema (Zod / `validateAndTransformBody`) and pass only known
107+
fields into the filter object.
108+
- **Dynamic column / order / table names** from user input (identifiers cannot
109+
be parameterized) — must be checked against an allow-list.
110+
- **`$where` / JS-expression style filters** or anything that lets the caller
111+
supply an expression string.
112+
113+
Fix expectation: use bound parameters for values, an allow-list for
114+
identifiers, and validate the request into a known shape before it reaches any
115+
filter or query builder. Never interpolate a tainted value into a query string.
116+
117+
## Output encoding — unescaped JSON, HTML, and prototype pollution
118+
119+
This is the most commonly missed class. A value can be safe in the DB yet
120+
dangerous when it is **serialized back out** without proper encoding:
121+
122+
- **Unescaped JSON embedded in HTML / a `<script>` context (XSS).** The classic
123+
bug is interpolating `JSON.stringify(data)` into an HTML string or inline
124+
script — e.g. `` `<script>window.__STATE__=${JSON.stringify(data)}</script>` ``
125+
or building an HTML email/template from user data. `JSON.stringify` does
126+
**not** escape HTML, so a value containing `</script>` (or `<!--`, `]]>`,
127+
U+2028/U+2029) breaks out of the script/tag and injects arbitrary markup.
128+
Fix: escape the dangerous characters (`<`, `>`, `&`, U+2028, U+2029) in the
129+
serialized string before embedding it, or pass the data via a `data-*`
130+
attribute / DOM API rather than string-concatenating it into HTML.
131+
- **User input reflected into any HTML/markup response** (server-rendered pages,
132+
emails, invoices, SVGs, redirects with a reflected param) without escaping →
133+
XSS/HTML injection.
134+
- **Building JSON by string concatenation** instead of `JSON.stringify` — a
135+
quote or brace in the value corrupts or injects into the document. Always
136+
serialize with `JSON.stringify`, never hand-assemble JSON.
137+
- **`JSON.parse` on untrusted input without a guard** — wrap in try/catch (a
138+
malformed body should be a 400, not an unhandled throw), and when the parsed
139+
object is **merged/assigned** into another object (`Object.assign`, spread,
140+
deep-merge, `lodash.merge`), treat `__proto__` / `constructor` / `prototype`
141+
keys as **prototype-pollution** vectors — reject or use a null-prototype
142+
target.
143+
- **`Content-Type` mismatch:** returning user-controlled text as `text/html`
144+
(or omitting the header so a browser sniffs it as HTML) when it should be
145+
`application/json` / `text/plain`.
146+
147+
## Authorization on data-scoped routes
148+
149+
For any new/changed route or workflow that reads or mutates data belonging to a
150+
user, store, or customer:
151+
152+
- Is there an ownership/scope check, or does it trust an ID from the request?
153+
- Is the auth/permission middleware actually applied to the new route?
154+
- Can a lower-privileged actor reach an admin-only operation?
155+
156+
## Other classes (see SKILL.md Step 10 for the full list)
157+
158+
- **Code execution:** `eval`/`Function`, dynamic `require`/`import` with user
159+
paths, shell construction from user input.
160+
- **SSRF:** user-controlled URLs in server-side `fetch`/HTTP calls without an
161+
allow-list.
162+
- **DoS:** missing size/length/pagination limits on user input.
163+
- **Data exposure:** secrets/PII/internal IDs in responses or logs, stack
164+
traces leaked to clients, hardcoded credentials.
165+
- **Supply chain:** typosquat packages, `pre/postinstall` scripts,
166+
lockfile/manifest mismatch.
167+
168+
## Severity and phrasing
169+
170+
- Every confirmed OR plausible security issue is **blocking** — it belongs in
171+
`blocking_points` with `"requires-more"` in `labels_to_add`, never merely
172+
mentioned in `summary`.
173+
- If you are not certain the input is reachable/tainted, still flag it as a
174+
question (*"Can `file.filename` contain `..`? If so this escapes the
175+
prefix…"*) and keep it blocking — the author must confirm or disprove.
176+
- Use `close-malicious` only for deliberate attacks (backdoors, exfiltration),
177+
never for good-faith bugs like the traversal above — those are
178+
`needs-changes`.
179+
- Phrase each finding as:
180+
*"`<file>:<location>`: `<vuln class>``<one-sentence attack scenario>`. Fix: `<concrete fix>`."*

0 commit comments

Comments
 (0)