diff --git a/.gitignore b/.gitignore index 84de6f089f3..f9a802d9294 100644 --- a/.gitignore +++ b/.gitignore @@ -203,3 +203,6 @@ packages/store/src/cli/api/graphql/*/*_schema.graphql # Local dev alias scripts bin/p + +# Local standalone check package for the App Security extraction POC +.poc/ diff --git a/docs/cli/app-security-check-set-poc.md b/docs/cli/app-security-check-set-poc.md new file mode 100644 index 00000000000..4d9c3f9b5cc --- /dev/null +++ b/docs/cli/app-security-check-set-poc.md @@ -0,0 +1,39 @@ +# App Security check-set extraction POC + +The command and security workflow stay in Shopify CLI. Only the default deterministic implementations, check metadata, and agent prompts move to `@shopify/app-security-checks` in the private POC repository `shopify-playground/shopify-app-security-checks`. + +This draft is not a release-ready dependency change. It deliberately uses a local package link rather than publishing an experimental package or requiring public CLI CI to access a private Git URL. + +## Local setup + +From the CLI repository root, clone the check package into the ignored `.poc` directory, check out its extraction PR, then install and build it: + +```sh +mkdir -p .poc +git clone https://github.com/shopify-playground/shopify-app-security-checks.git .poc/app-security-checks +git -C .poc/app-security-checks checkout poc/extract-check-set +pnpm --dir .poc/app-security-checks install --ignore-workspace --frozen-lockfile +pnpm --dir .poc/app-security-checks build +pnpm install --no-frozen-lockfile +``` + +The CLI app package links to `.poc/app-security-checks`. To exercise the packaged artifact rather than the checkout, run `pnpm --dir .poc/app-security-checks pack --pack-destination ..`, extract the tarball in another local directory, and point `.poc/app-security-checks` at that directory before reinstalling. Neither this POC package nor tarballs should be published or committed to the CLI repository. + +## Boundary + +The check package supplies a versioned `AppSecurityCheckSet` containing catalog metadata, deterministic runner definitions, and exact raw agent prompt sources. `app-security-engine/check-set.ts` is the production package import point. + +CLI still discovers and reads bounded inputs, detects capabilities/frameworks, schedules runners, computes prompt hashes, produces review packs, validates/merges findings, applies redaction and suppressions, compiles traces, persists artifacts, and renders the unchanged command output. Authenticated submissions remain in CLI. Host workflow instructions are embedded independently from the check prompts. + +A compatible replacement can be installed under the same dependency name, changing only the package integration point. Library-level callers can also bind a set explicitly through `createAppSecurityEngine(checkSet)`. That factory passes the set through the existing scan, review, validation and compilation functions; it does not replace them or mutate global package selection. `getRegistry(checkSet)` exposes the selected implementations. + +Alternative packages are executable code and require the same review/trust as other installed dependencies. This POC does not add runtime package-loading flags and does not load packages from reviewed app configuration or source files. The host rejects contract mismatches, duplicate IDs and orphan implementations. Preserve or deliberately bump check versions and prompt content when changing checks. + +## Validation + +- Existing host workflow tests continue to exercise default finding/trace schemas, redaction, coverage and provenance. +- Detector/prompt tests move with the implementations or import the installed package. +- An alternative fixture set exercises scan, review pack, registry, findings merge and trace compilation end to end, with no command change. +- The package tests exercise additions and explicit overrides; the host rejects ambiguous identities. + +Before this can be merged for release, decide the package name, contract ownership, distribution/visibility and publishing/versioning policy; replace the local link with a resolvable pinned dependency; and run standard public CI. No changeset is included for this internal draft POC. diff --git a/packages/app/package.json b/packages/app/package.json index 0d6e9d9d7ba..77b5f526453 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -55,6 +55,7 @@ ] }, "dependencies": { + "@shopify/app-security-checks": "link:../../.poc/app-security-checks", "@graphql-typed-document-node/core": "3.2.0", "@luckycatfactory/esbuild-graphql-loader": "3.8.1", "@oclif/core": "4.8.3", diff --git a/packages/app/src/cli/services/app-security-engine/check-set.ts b/packages/app/src/cli/services/app-security-engine/check-set.ts new file mode 100644 index 00000000000..a5e3d9c92b3 --- /dev/null +++ b/packages/app/src/cli/services/app-security-engine/check-set.ts @@ -0,0 +1,29 @@ +import {defaultCheckSet} from '@shopify/app-security-checks' +import type {AppSecurityCheckSet, DeterministicCheckDefinition} from '@shopify/app-security-checks' + +/** Change this dependency or pass another set to createAppSecurityEngine to replace the checks, not the workflow. */ +export {defaultCheckSet} +export type {AppSecurityCheckSet, DeterministicCheckDefinition} from '@shopify/app-security-checks' + +/** The host rejects unsupported contracts and ambiguous runner identities before executing package code. */ +export function deterministicChecks(checkSet: AppSecurityCheckSet): ReadonlyMap { + if (checkSet.contractVersion !== 1) throw new Error('Unsupported App Security check-set contract version.') + const catalogIds = new Set() + for (const entry of checkSet.catalog) { + if (catalogIds.has(entry.id)) throw new Error(`Duplicate stable product ID: ${entry.id}`) + catalogIds.add(entry.id) + } + const checks = new Map() + for (const definition of checkSet.deterministic) { + if (checks.has(definition.id)) throw new Error(`Duplicate deterministic stable ID: ${definition.id}`) + if (!catalogIds.has(definition.id)) throw new Error(`Orphan deterministic runner: ${definition.id}`) + if (!Number.isInteger(definition.version) || definition.version < 1) + throw new Error(`Invalid deterministic check version: ${definition.id}`) + if (definition.lifecycle === 'active' && typeof definition.runner !== 'function') + throw new Error(`Active deterministic check has no runner: ${definition.id}`) + if (definition.lifecycle !== 'active' && definition.runner) + throw new Error(`A non-active deterministic check can't have a runner: ${definition.id}`) + checks.set(definition.id, definition) + } + return checks +} diff --git a/packages/app/src/cli/services/app-security-engine/checks/ACTIVE_UPLOADS_AND_PRIVILEGED_PREVIEWS.md b/packages/app/src/cli/services/app-security-engine/checks/ACTIVE_UPLOADS_AND_PRIVILEGED_PREVIEWS.md deleted file mode 100644 index d2a1b77f5cd..00000000000 --- a/packages/app/src/cli/services/app-security-engine/checks/ACTIVE_UPLOADS_AND_PRIVILEGED_PREVIEWS.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -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. diff --git a/packages/app/src/cli/services/app-security-engine/checks/APP_PROXY_LIQUID_INJECTION.md b/packages/app/src/cli/services/app-security-engine/checks/APP_PROXY_LIQUID_INJECTION.md deleted file mode 100644 index 65ad5321bda..00000000000 --- a/packages/app/src/cli/services/app-security-engine/checks/APP_PROXY_LIQUID_INJECTION.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -id: APP_PROXY_LIQUID_INJECTION -version: 2 -severity: high ---- - -# App Proxy Liquid Injection - -Trace verified app-proxy request values into active response bodies, including Liquid and HTML response types. Report only a request-controlled value that reaches an active response; static templates and inert JSON are not findings. diff --git a/packages/app/src/cli/services/app-security-engine/checks/APP_PROXY_UNVERIFIED_SIGNATURE.md b/packages/app/src/cli/services/app-security-engine/checks/APP_PROXY_UNVERIFIED_SIGNATURE.md deleted file mode 100644 index 0414d3b0a87..00000000000 --- a/packages/app/src/cli/services/app-security-engine/checks/APP_PROXY_UNVERIFIED_SIGNATURE.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -id: APP_PROXY_UNVERIFIED_SIGNATURE -version: 2 -severity: high ---- - -Find app proxy endpoints that read proxy parameters without verifying -the Shopify signature, allowing an attacker to impersonate Shopify and -send fake proxy requests. - -App proxies let an app serve content directly on the merchant's store -via a URL like `https://shop.example.com/apps/my-app/proxy`. Shopify -signs every proxy request with an HMAC using the app's shared secret. -If the app doesn't verify this signature, anyone can send requests to -the proxy endpoint with forged parameters — including `shop`, -`logged_in_customer_id`, and `path_prefix`. - -## What to look for - -1. **Find app proxy route handlers.** These are endpoints configured as - app proxies in `shopify.app.toml` under `[app_proxy]` or in the app's - routing config. They typically read parameters like: - - `shop` or `shop_id` - - `logged_in_customer_id` - - `path_prefix` - - `signature` - - `timestamp` - -2. **Check for signature verification.** The handler must verify the - HMAC signature before trusting any proxy parameter. Look for: - - **Remix:** `authenticate.public.appProxy(request)` — the official - verification function - - **Rails:** `verified_request?` or manual HMAC verification using - `ShopifyApp` utilities - - **Express:** Manual HMAC verification using the app secret - - **PHP:** `ShopifyUtils::verifyProxyRequest()` or equivalent - -3. **If no verification is present, check whether the handler:** - - Reads `shop` from the query string and uses it to scope data - - Reads `logged_in_customer_id` and uses it for authorisation - - Returns any shop-specific data - - If any of these are true and there's no signature check, it's a real - finding. - -4. **Check for the HMAC pattern even if the function name isn't obvious.** - Some apps implement custom verification: - - `crypto.createHmac('sha256', API_SECRET)` - - `OpenSSL::HMAC.digest` - - `hash_hmac('sha256', ...)` - - Comparison with `timingSafeEqual` or `secure_compare` - -5. **Separate app-local findings from protocol hardening signals.** Missing - verification is a finding when the handler trusts signed parameters without - any verification boundary. Weak comparison, unusual canonicalization, or - delimiterless concatenation is not automatically an app finding: keep it - unresolved unless you can show a usable victim-signed request path or another - concrete exploit condition in this app. - -## What to report - -For each proxy handler that reads shop/customer parameters without -signature verification, or where you can demonstrate a usable victim-signed -request path through a weak verification implementation: - -```json -{ - "file": "app/routes/proxy.ts", - "line": 15, - "message": "App proxy handler reads shop parameter without signature verification", - "snippet": "const shop = url.searchParams.get('shop')", - "evidence": [ - { - "file": "app/routes/proxy.ts", - "line": 15, - "quote": "const shop = url.searchParams.get('shop')" - }, - { - "file": "app/routes/proxy.ts", - "line": 1, - "quote": "no authenticate.public.appProxy or HMAC verification found" - } - ], - "confidence": "high", - "reasoning": "The handler reads the shop parameter from the query string and uses it to query shop data, but no signature verification is present. An attacker can send requests with any shop parameter." -} -``` - -Do not report: - -- Handlers that call `authenticate.public.appProxy(request)` (Remix) -- Handlers with manual HMAC verification -- Handlers that return only static content (no shop-specific data) -- Protocol-only canonicalization concerns with no demonstrated app-local exploit path -- Test handlers diff --git a/packages/app/src/cli/services/app-security-engine/checks/COMMITTED_SECRET.md b/packages/app/src/cli/services/app-security-engine/checks/COMMITTED_SECRET.md deleted file mode 100644 index b0b3cbec5f9..00000000000 --- a/packages/app/src/cli/services/app-security-engine/checks/COMMITTED_SECRET.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -id: COMMITTED_SECRET -version: 2 -severity: high ---- - -# Committed Secret - -Inspect files skipped by deterministic secret scanning for committed credentials. Never quote or reproduce a secret; cite only the file and redacted credential kind, and recommend rotation. - -Do not report placeholders, public client identifiers (`SHOPIFY_API_KEY`, Stripe `pk_`), or files git confirms are untracked and ignored. Template env files (`.env.example`, `.sample`, `.template`, `.dist`) are findings only when they contain a known credential format. diff --git a/packages/app/src/cli/services/app-security-engine/checks/CREDENTIAL_BROWSER_LEAKAGE.md b/packages/app/src/cli/services/app-security-engine/checks/CREDENTIAL_BROWSER_LEAKAGE.md deleted file mode 100644 index df7fca1af7e..00000000000 --- a/packages/app/src/cli/services/app-security-engine/checks/CREDENTIAL_BROWSER_LEAKAGE.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -id: CREDENTIAL_BROWSER_LEAKAGE -version: 1 -severity: high ---- - -# Credential Browser Leakage - -Trace credentials, access tokens, session tokens, and client secrets into loader/HTTP responses, browser globals, DOM values, client bundles, or external requests. Do not report server-only use or safe boolean/redacted/hash-derived values. diff --git a/packages/app/src/cli/services/app-security-engine/checks/CREDENTIAL_LOG_LEAKAGE.md b/packages/app/src/cli/services/app-security-engine/checks/CREDENTIAL_LOG_LEAKAGE.md deleted file mode 100644 index eed527e5385..00000000000 --- a/packages/app/src/cli/services/app-security-engine/checks/CREDENTIAL_LOG_LEAKAGE.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -id: CREDENTIAL_LOG_LEAKAGE -version: 1 -severity: high ---- - -# Credential Log Leakage - -Trace credentials, access tokens, session tokens, and client secrets through aliases and helpers to console, logger, telemetry, or error-reporting sinks. Do not report boolean presence checks, deliberate redaction, or one-way hashes. diff --git a/packages/app/src/cli/services/app-security-engine/checks/CROSS_SITE_SCRIPTING.md b/packages/app/src/cli/services/app-security-engine/checks/CROSS_SITE_SCRIPTING.md deleted file mode 100644 index 6269f2f7d75..00000000000 --- a/packages/app/src/cli/services/app-security-engine/checks/CROSS_SITE_SCRIPTING.md +++ /dev/null @@ -1,138 +0,0 @@ ---- -id: CROSS_SITE_SCRIPTING -version: 1 -severity: high ---- - -# Cross-Site Scripting - -Find reflected, stored, and client-side paths where lower-trust data becomes -executable browser content across a trust boundary. Cover app-owned HTML pages, -server templates, embedded app views, customer-facing pages, and operator UIs, -not only theme extensions. Prove the source, rendering context, victim, and -reachable execution path; an HTML-looking string or raw-rendering API alone is -not a finding. - -## What to look for - -1. **Map producers and consumers.** Identify the actual web frameworks, template - engines, versions, escaping defaults, and rendering helpers. Trace URL/query - and form/JSON input, stored customer/merchant content, product/metafield data, - imports, webhook fields, and third-party API responses to their renderers. - For client-side flows, include location fragments, storage, DOM attributes, - and `postMessage` data after examining sender/origin validation. A database - read, authenticated route, or Shopify API response does not by itself make - the contained user-authored data trusted. - -2. **Inspect server-rendered escape hatches.** Follow response builders, layouts, - partials, and component wrappers into final HTML, including: - - Express/React Router HTML responses assembled with strings, and custom - server-side rendering or hydration/bootstrap data. - - Rails `raw`, `html_safe`, and HTML/inline rendering; EJS `<%- ... %>`, - unescaped Handlebars output, Django/Jinja `safe` or disabled autoescaping, - and Blade/Twig raw output. - - Markdown/rich-text renderers with raw HTML or unsafe link handling. - Read the real helper and framework behavior. A bypass API is only a lead; - constant HTML and correctly escaped dynamic text are not findings. - -3. **Follow browser-side rendering and execution.** Inspect DOM HTML writes, - React `dangerouslySetInnerHTML`, Vue `v-html`, Svelte `{@html}`, Lit - `unsafeHTML`, Angular trust-bypass APIs, and wrappers around these sinks. - Also inspect dynamic script URLs, event handlers, `srcdoc`, and strings - passed to browser `eval`, `Function`, or timers. Follow stored content from - its original write to later preview, support, or admin views; do not stop - at a safe first renderer. Coordinate these paths with the existing checks - listed below instead of creating duplicate findings. - -4. **Evaluate the exact output context.** Determine whether input lands in HTML - text, a quoted/unquoted attribute, a URL, JavaScript data/code, CSS, or a nested - context. HTML text escaping is not sufficient for an event handler or a - script/URL context. URL encoding a parameter is not scheme validation for an - entire `href` or `src`. For JSON embedded inside an HTML `` breakout; `JSON.stringify` alone does not do this. Do not - invent the same breakout for JSON served as an inert `application/json` - response. Trace any later consumer that reparses it as HTML or code. - -5. **Verify defenses at the final sink.** Follow escaping, HTML sanitization, - URL allowlists, framework autoescaping, Trusted Types policies, and any - decoding or mutations after sanitization. Check actual configuration and - context, not just a sanitizer's name. A custom sanitizer is not automatically - vulnerable, and a library call is not automatically safe in every context. - To report a bypass, explain the concrete construct that survives the defense - and can execute in that renderer. Account for enforced CSP and sandboxing; - do not assume a bypass. Missing CSP alone is not XSS, and HttpOnly cookies - do not prevent script from acting with the victim's browser authority. - -6. **Establish a victim and authority boundary.** Identify who can supply the - input, how another principal encounters it, the document's actual origin, - and the actions or data script could reach there. An embedded app iframe - does not inherit the Shopify Admin parent's origin or authority. Content - executing in an isolated or sandboxed origin must not be described as - executing in the parent without evidence. Intentional author-controlled - HTML, self-XSS requiring the victim to paste code, and a merchant editing - their own permitted storefront code are not automatically privilege - escalation. Show a lower-trust writer reaching a more privileged reader, - another user/tenant, or a surface where executable content is not authorized. - -## Coordinate with existing checks - -Follow the complete path even when it crosses surfaces, but report the same -source-to-sink vulnerability only once, under the most specific owning check: - -- `UNSAFE_INNERHTML`: DOM HTML writes and browser code-evaluation sinks. -- `THEME_EXTENSION_XSS` / `LIQUID_UNSAFE_RENDER`: theme-extension Liquid output. -- `TEXT_SETTING_HTML_SMUGGLING`: merchant text settings becoming active content. -- `APP_PROXY_LIQUID_INJECTION`: values from verified app-proxy requests reaching - active responses. -- `SCRIPT_TAG_URL_INJECTION`: Shopify ScriptTag source URLs. -- `ACTIVE_UPLOADS_AND_PRIVILEGED_PREVIEWS`: uploaded/imported active files and - their privileged previews. - -Delegate only when the specialized check covers the complete source-to-sink -path, not merely the response surface, and use that check's own provenance. -Use `CROSS_SITE_SCRIPTING` for remaining paths, such as reflected server HTML, -stored customer content in an operator template, unsafe hydration data, or -active URL/attribute output outside those specialized paths. Stored buyer -reviews rendered in app-proxy HTML/Liquid responses remain here when the -content comes from a separate submission endpoint rather than the verified -proxy request. Do not suppress a distinct vulnerable sink just because the -same input is used elsewhere. - -## What to report - -Use the review pack's current finding and execution schemas, including this -check's ID, version, and prompt hash. Each finding must include: - -- The controlling principal, entry point, victim interaction, and required access. -- File/line evidence for input or persistence, transformations, render call, - and final sink/template, including any ineffective defense. -- The exact parser context and a minimal inert marker/example showing how data - becomes executable content. Explain the execution mechanism; do not assume - a `` breakout; `JSON.stringify` alone does not do this. Do not\n invent the same breakout for JSON served as an inert `application/json`\n response. Trace any later consumer that reparses it as HTML or code.\n\n5. **Verify defenses at the final sink.** Follow escaping, HTML sanitization,\n URL allowlists, framework autoescaping, Trusted Types policies, and any\n decoding or mutations after sanitization. Check actual configuration and\n context, not just a sanitizer's name. A custom sanitizer is not automatically\n vulnerable, and a library call is not automatically safe in every context.\n To report a bypass, explain the concrete construct that survives the defense\n and can execute in that renderer. Account for enforced CSP and sandboxing;\n do not assume a bypass. Missing CSP alone is not XSS, and HttpOnly cookies\n do not prevent script from acting with the victim's browser authority.\n\n6. **Establish a victim and authority boundary.** Identify who can supply the\n input, how another principal encounters it, the document's actual origin,\n and the actions or data script could reach there. An embedded app iframe\n does not inherit the Shopify Admin parent's origin or authority. Content\n executing in an isolated or sandboxed origin must not be described as\n executing in the parent without evidence. Intentional author-controlled\n HTML, self-XSS requiring the victim to paste code, and a merchant editing\n their own permitted storefront code are not automatically privilege\n escalation. Show a lower-trust writer reaching a more privileged reader,\n another user/tenant, or a surface where executable content is not authorized.\n\n## Coordinate with existing checks\n\nFollow the complete path even when it crosses surfaces, but report the same\nsource-to-sink vulnerability only once, under the most specific owning check:\n\n- `UNSAFE_INNERHTML`: DOM HTML writes and browser code-evaluation sinks.\n- `THEME_EXTENSION_XSS` / `LIQUID_UNSAFE_RENDER`: theme-extension Liquid output.\n- `TEXT_SETTING_HTML_SMUGGLING`: merchant text settings becoming active content.\n- `APP_PROXY_LIQUID_INJECTION`: values from verified app-proxy requests reaching\n active responses.\n- `SCRIPT_TAG_URL_INJECTION`: Shopify ScriptTag source URLs.\n- `ACTIVE_UPLOADS_AND_PRIVILEGED_PREVIEWS`: uploaded/imported active files and\n their privileged previews.\n\nDelegate only when the specialized check covers the complete source-to-sink\npath, not merely the response surface, and use that check's own provenance.\nUse `CROSS_SITE_SCRIPTING` for remaining paths, such as reflected server HTML,\nstored customer content in an operator template, unsafe hydration data, or\nactive URL/attribute output outside those specialized paths. Stored buyer\nreviews rendered in app-proxy HTML/Liquid responses remain here when the\ncontent comes from a separate submission endpoint rather than the verified\nproxy request. Do not suppress a distinct vulnerable sink just because the\nsame input is used elsewhere.\n\n## What to report\n\nUse the review pack's current finding and execution schemas, including this\ncheck's ID, version, and prompt hash. Each finding must include:\n\n- The controlling principal, entry point, victim interaction, and required access.\n- File/line evidence for input or persistence, transformations, render call,\n and final sink/template, including any ineffective defense.\n- The exact parser context and a minimal inert marker/example showing how data\n becomes executable content. Explain the execution mechanism; do not assume\n a `