Repository navigation
Stop app security prompts from passing unsafe code as safe - #9107
Open
isaacroldan wants to merge 1 commit into
Open
isaacroldan wants to merge 1 commit into
isaacroldan wants to merge 1 commit into
Conversation
Four agent check prompts could mark a real issue not applicable or safe: - MISSING_EMBEDDED_CSP decided whether an app is embedded from app_embed or theme_app_extension, which describe storefront theme extensions. It now uses embedded = true from shopify.app*.toml, the signal the engine already reads as embedded_app, and names the React Router SDK's addDocumentResponseHeaders. - SSRF_REQUEST_FORGERY counted an https-only scheme, or an internal-IP blocklist alone, as protection. It now requires a host allowlist with redirects disabled or validated. - SCRIPT_TAG_URL_INJECTION counted an https-only scheme as protection. It now requires an allowlist or a fixed set of trusted, versioned assets. - THEME_EXTENSION_XSS exempted all escape output and all textContent writes, contradicting step 2. The exemptions now exclude executable contexts and <script> elements. Co-authored-by: Isaac Roldan <[email protected]>
isaacroldan
force-pushed
the
clisec/CLISEC-21
branch
from
November 9, 2026 11:04
399c5d2 to
ad9e515
Compare
isaacroldan
marked this pull request as ready for review
November 9, 2026 11:05
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
WHY are these changes introduced?
A team that runs
shopify app security checkin CI found four agent check prompts that can make an agent skip a real issue. Each prompt either checks the wrong signal or counts a control as protection when the check's shopify.dev page does not:MISSING_EMBEDDED_CSPdecided whether an app is embedded fromapp_embedortheme_app_extension.app_embedis not ashopify.app.tomlkey. In the engine it is a capability for a theme extension with an app embed block, which is a storefront feature. So an embedded admin app with no theme extension was marked not applicable, and the prompt's example quotedapp_embed = true.SSRF_REQUEST_FORGERYcounted "URL scheme is restricted to https only", or an internal-IP blocklist alone, as protection. Anhttps://URL can still point at an internal host. The docs ask for a host allowlist that also checks the scheme, and for redirect handling.SCRIPT_TAG_URL_INJECTIONcounted "restricted to https" as protection. An attacker's own server serves https. The docs ask for trusted, versioned assets.THEME_EXTENSION_XSSexempted all| escapeoutput and alltextContentwrites in its "Do not report" list. That contradicted step 2, which says HTML escaping is not enough in event handlers,srcdoc, or<script src>. SettingtextContenton a<script>element sets the code it runs.WHAT is this pull request doing?
MISSING_EMBEDDED_CSP(v4) usesembedded = truefromshopify.app*.toml, the signal the engine already reads asembedded_app. It also names@shopify/shopify-app-react-routerbeside the Remix SDK foraddDocumentResponseHeaders.SSRF_REQUEST_FORGERY(v2) treats a call with user input as safe only with a host allowlist plus disabled or validated redirects. An https-only scheme or an IP blocklist alone is not enough.SCRIPT_TAG_URL_INJECTION(v3) drops the https bullet. It accepts an allowlist of domains the app controls, or a fixed set of trusted, versioned assets. A path on the app's own domain is safe only when no user-controlled content can be served from it.THEME_EXTENSION_XSS(v4) limits theescapeexemption to HTML text and quoted ordinary attributes, and limits thetextContentexemption to elements other than<script>.checks/embedded.ts, updates the version expectations, and adds a regression test.How to manually test your changes?
Run
shopify app security checkwith agent checks on an embedded app (embedded = true) that has no theme app extension and does not calladdDocumentResponseHeaders. The agent now evaluatesMISSING_EMBEDDED_CSPinstead of marking it not applicable.Patch details
Shopify/climainatc421dbe844122d551b26d1dacd4e187ae3ed18f8.git am < CLISEC-21.patch.Validation
pnpm vitest run src/cli/services/app-security-engine/tests/inpackages/app: 22 files, 559 tests passed.pnpm vitest run src/cli/services/security src/cli/services/app-securityinpackages/app: 48 files, 1183 tests passed, 3 skipped.pnpm type-checkinpackages/app: passed.eslintandprettier --checkon the changed TypeScript and changeset: passed.node embed-checks.mjs --check:embedded.tsis up to date.git amon the base.Before and after agent runs
Each check ran against small fixture apps with the old prompt and with the new prompt, 5 times each, on
claude-sonnet-latestandclaude-haiku-latest. One isolated pass had the CLI's investigation and status rules, one check, and every fixture file inline. This approximates the real CLI flow, where an agent explores a repository. It does not replace it. The table counts runs that gave the correct result, out of 10 (5 per model).srcfrom a form, checked for https onlysrcfrom a fixed map of versioned assetsfetchof a form URL, checked for https onlyfetchwith a host allowlist andredirect: "error"embedded = true, no theme extension, no CSP, destructive actionembedded = truewithaddDocumentResponseHeadersembedded = falsewith a theme app extensionnot_applicableonclickwithescapetextContenton a new<script>from a shared-namespace metafieldhrefwithescapetextContenton a<div>What this shows:
SCRIPT_TAG_URL_INJECTION: the old prompt missed an https-only check every time. The new prompt catches it every time.SSRF_REQUEST_FORGERY: the old prompt missed once. The new prompt never missed.MISSING_EMBEDDED_CSP: the reported miss did not reproduce. Both models readembedded = trueeven though the old prompt points toapp_embed. For a non-embedded app with a theme extension, the old prompt gaveunresolvedorexecutedin 5 of 10 runs. The new prompt gavenot_applicableevery time.THEME_EXTENSION_XSS: no difference. The old prompt caught all three unsafe fixtures. The change removes the contradiction in the prompt text, but these runs show no change in behavior.Requested by Isaac Roldan [email protected]
Torrent ticket: CLISEC-21