Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/bright-app-doctor-checks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/app': patch
---

Add App Doctor agent checks for lifecycle replay, dependency reachability, and active upload previews.
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
id: ACTIVE_UPLOADS_AND_PRIVILEGED_PREVIEWS
version: 1
severity: high
---

# Active Uploads And Privileged Previews

Find cases where merchant-, customer-, webhook-, or external-service-supplied
files become active content in a privileged origin. Trace uploads, imports,
previews, and generated assets from ingestion through storage and final render.

The risk is not the upload alone. The risk is an untrusted-upload-to-active-render
path: SVG, HTML, XML, PDF, blob/data URL, or another active format is accepted and
later rendered in a storefront, embedded admin, customer-account, theme-editor,
or operator/admin context where it can execute or leak protected data.

## What to look for

1. **Find upload and import entry points.** Search for file uploads, import jobs,
webhook attachments, remote fetches, document parsers, blob/data URL handling,
and generated preview endpoints.

2. **Trace file metadata and validation.** Check size limits, extension checks,
declared MIME type, magic-byte/file-signature verification, filename handling,
generated storage names, antivirus/sanitization, and any image/PDF re-encoding.

3. **Inspect storage and serving boundaries.** Determine whether the object is
stored on a non-executable origin, served with explicit `Content-Type` and
`Content-Disposition`, and prevented from inheriting privileged cookies or
browser authority.

4. **Follow every final renderer.** Check storefront/theme renderers, embedded
admin previews, customer-account views, email/PDF previews, admin/operator
tools, iframe/srcdoc/blob/data URL renderers, and any browser code that inserts
the uploaded content into the DOM.

5. **Check sandboxing and isolation.** Verify iframes, preview origins, CSP,
download headers, SVG sanitization, PDF handling, and re-encoding before
deciding the content is safe.

## What to report

Report a finding only for a complete untrusted-upload-to-active-render path where
the uploaded or imported object is actually rendered or served into a privileged
executable context. Show:
- who controls the uploaded/imported content;
- which validation or isolation boundary is missing;
- where the content becomes active or executable;
- which privileged origin or user is affected; and
- file/line evidence for both the ingest path and the renderer/serving path.

Example:

```json
{
"file": "app/controllers/previews_controller.rb",
"line": 28,
"message": "Uploaded SVG is rendered inline in the admin preview without sanitization or origin isolation",
"evidence": [
{ "file": "app/controllers/uploads_controller.rb", "line": 14, "quote": "params[:file]" },
{ "file": "app/controllers/previews_controller.rb", "line": 28, "quote": "render inline: blob.download" }
],
"confidence": "high",
"reasoning": "The merchant-controlled SVG is stored without re-encoding and later rendered inline in the embedded admin origin, so script-capable SVG content can execute with merchant authority."
}
```

Do not report:
- files that are forced to download and never rendered in an active origin;
- images/PDFs that are re-encoded or sanitized before serving;
- isolated preview origins with no privileged cookies, storage, or message bridge;
- missing deployment details where you cannot establish executable rendering or unsafe serving.
- permissive content types, inline disposition, or storage/header hygiene issues
without a concrete privileged renderer or execution surface.
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
id: DEPENDENCY_REACHABILITY
version: 1
severity: medium
---

# Dependency Reachability

Start from a static dependency or SDK finding and determine whether the app is
actually exposed to the vulnerable behavior. A vulnerable package version is not
enough by itself: confirm that the app uses the vulnerable API or helper, enables
the affected configuration, and exposes the relevant trust boundary.

## What to look for

1. **Identify the vulnerable version and advisory scope.** Read the manifest,
lockfile, or deterministic finding and determine the exact package, version,
vulnerable range, and affected API/helper/configuration from the advisory or
shipped Shopify SDK behavior.

2. **Find imports and call sites.** Search for direct imports, wrapper helpers,
generated clients, middleware, framework adapters, or transitive call paths
that reach the vulnerable API or helper.

3. **Check configuration and feature gates.** Some vulnerabilities only apply
when a flag, transport, parser mode, canonicalization shape, or optional
feature is enabled. Verify the app actually enables the affected path.

4. **Trace the reachable impact.** Confirm which untrusted input can reach the
vulnerable dependency behavior and what authority or data is exposed if the
bug triggers.

5. **Distinguish version exposure from exploitability.** If the vulnerable
package is present but the app never calls the affected API/helper, or the
vulnerable configuration is disabled, keep the result unresolved rather than
reporting a finding.

## What to report

Report a finding only when you can show:
- the vulnerable package or Shopify SDK version;
- the vulnerable API or helper in use;
- the enabling configuration or call shape;
- the untrusted input or trigger; and
- the resulting security impact.

Example:

```json
{
"file": "app/services/session_verifier.ts",
"line": 18,
"message": "App uses vulnerable session-token helper without issuer validation",
"evidence": [
{ "file": "package.json", "line": 12, "quote": "\"@shopify/shopify-app-remix\": \"x.y.z\"" },
{ "file": "app/services/session_verifier.ts", "line": 18, "quote": "verifySessionToken(token)" }
],
"confidence": "high",
"reasoning": "The installed SDK version contains the vulnerable helper implementation and the app calls that helper on attacker-controlled session tokens without an issuer check."
}
```

Do not report:
- a vulnerable version with no reachable use of the affected API/helper;
- dev-only tooling or test-only dependencies that cannot affect production;
- advisories whose required configuration is not enabled in this app;
- guessed exploitability when the call path or trigger cannot be established from source.
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
id: SESSION_LIFECYCLE_AND_REPLAY
version: 1
severity: high
---

# Session Lifecycle And Replay

Find security-sensitive lifecycle gaps where a session, token, signed URL,
capability, or state-changing endpoint remains usable after the app should have
invalidated it, or where a request can be replayed to repeat a privileged action.

Focus on stale sessions after logout or uninstall, role/permission downgrades,
replayable order or redemption endpoints, and cookie-authenticated state changes
whose protection disappears outside the happy path.

## What to look for

1. **Map the lifecycle boundaries.** Identify login, token issuance, refresh,
logout, uninstall, revocation, account disconnect, role change, and feature
disablement flows. Find where sessions, refresh tokens, signed links, and
capability records are created, rotated, and deleted.

2. **Find replayable sensitive actions.** Search for order fulfillment,
redemption, refund, payout, invitation, export, configuration, and mutation
endpoints that can be invoked more than once. Check for nonce, idempotency,
consumed-token, replay-window, or state-transition guards.

3. **Trace stale artifacts after lifecycle changes.** Verify that logout,
uninstall, token revocation, shop disconnect, or role downgrade invalidates
every downstream session, refresh token, signed URL, cache entry, webhook
capability, and background-job credential that could still authorize work.

4. **Check cookie-authenticated state changes.** For server-rendered or
cookie-backed flows, verify CSRF protection still applies on replayed direct
URLs, stale links, and downgraded sessions. Session presence alone is not proof
that the operation is still authorized.

5. **Compare the first successful action with later retries.** A create path may
be authorized once but later update/delete/redeem/replay paths may skip the
same checks. Follow the full state machine, not just the initial handler.

## What to report

Report a finding only for a complete lifecycle or replay path where you can show:
- the principal or caller;
- the stale or replayable artifact;
- the missing invalidation, idempotency, or re-authorization boundary;
- the sensitive action that remains reachable; and
- the affected shop, user, customer, or financial authority.

Example:

```json
{
"file": "app/controllers/redemptions_controller.rb",
"line": 42,
"message": "Redeem endpoint accepts the same signed link after the reward was already consumed",
"evidence": [
{ "file": "app/controllers/redemptions_controller.rb", "line": 42, "quote": "Reward.find(params[:id]).redeem!" },
{ "file": "app/models/reward.rb", "line": 18, "quote": "def redeem!" }
],
"confidence": "high",
"reasoning": "The signed redemption URL remains valid after the first redemption and no consumed-token or state-transition guard runs before issuing the reward again."
}
```

Do not report:
- safe retries protected by idempotency keys, consumed-token state, or replay windows;
- stateless GET requests that expose no protected data or side effect;
- cleanup code where you cannot show a stale credential or replayed action remains usable;
- theoretical lifecycle concerns with no demonstrated stale session, replay, or sensitive action.
Loading
Loading