Skip to content

Detect React Router apps outside the app directory in app security check - #8827

Merged
jplhomer merged 12 commits into
mainfrom
jplhomer/app-security-monorepo-react-router-detection
Oct 9, 2026
Merged

jplhomer merged 12 commits into
mainfrom
jplhomer/app-security-monorepo-react-router-detection

Conversation

@jplhomer

@jplhomer jplhomer commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

WHY are these changes introduced?

Refs shop/issues-develop#24160 (checklist item 3)

shopify app security check only treats the app directory as a React Router code root: it looks for app/routes/ and app/shopify.server.* relative to it. In monorepos the React Router server often lives elsewhere, such as packages/server added with --include-dir, or web/ under a root shopify.app.toml. Those files are already gathered, but the framework is reported as unknown, so nine deterministic checks are gated off as unsupported_framework or only partly run: UNAUTHENTICATED_ENDPOINT, REQUEST_CONTROLLED_ADMIN_CONTEXT, CREDENTIAL_LOG_LEAKAGE, CREDENTIAL_BROWSER_LEAKAGE, APP_PROXY_LIQUID_INJECTION, DEPRECATED_SCRIPT_TAG_SCOPE, UNSAFE_INNERHTML, EOL_API_VERSION and EXPIRING_OFFLINE_TOKEN.

WHAT is this pull request doing?

  • Detect React Router roots as the directory of each gathered package.json that declares @shopify/shopify-app-react-router and has app/routes/ plus app/shopify.server.*. The app directory keeps its existing rule, so flat apps are detected exactly as before.
  • Leave out roots inside another app's directory (the directories already reported as otherAppDirectories), so a sibling app in scope doesn't become this app's React Router root.
  • Make the roots available to rules on the scan context, and match EOL_API_VERSION shopify.server declarations under each root instead of only app/shopify.server.*.

The roots are on the scan context rather than ProjectDetection, because ProjectDetection is written as the detection block of the deterministic findings document. This keeps that output unchanged.

Not included: EXPIRING_OFFLINE_TOKEN still reads every shopifyApp( setup in the gathered files, so a sibling app in scope can still cause a false positive there. That's tracked separately as checklist item 4.

How to manually test your changes?

  1. In a monorepo, put a shopify.app.toml in apps/foo and a React Router app (package.json with @shopify/shopify-app-react-router, app/routes/, app/shopify.server.ts) in packages/server.
  2. Run shopify app security check --path apps/foo --include-dir packages/server.
  3. The framework is reported as React Router, and the source checks listed above run instead of reporting an unsupported framework.

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've considered possible documentation changes
  • I've considered analytics changes to measure impact
  • The change is user-facing — I've identified the correct bump type (patch for bug fixes · minor for new features · major for breaking changes) and added a changeset with pnpm changeset add

The app security check only looked for app/routes and app/shopify.server in
the app directory, so React Router code gathered from an --include-dir or a
web directory gated nine deterministic checks off as unsupported_framework.

Find React Router roots from each gathered package.json that declares
@shopify/shopify-app-react-router, and match EOL_API_VERSION source
declarations under each root. Roots inside another app directory are left out.

Refs shop/issues-develop#24160

Co-authored-by: Qlaw <noreply@qlaw.quick.shopify.io>
@jplhomer
jplhomer requested a review from a team as a code owner October 7, 2026 15:45
Copilot AI balanced review requested due to automatic review settings October 7, 2026 15:45
@github-actions github-actions Bot added the Area: @shopify/app @shopify/app package issues label Oct 7, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The focused detection change has regression coverage and no unresolved findings; tests were not executed.

0 open findings

What changed in this PR

Extends app security checks to detect React Router code in monorepo packages and nested directories without changing the serialized detection format.

Changes:

  • Detects additional React Router roots while excluding roots belonging to other apps.
  • Uses detected roots for API-version checks and adds regression coverage.
File Description
packages/​app/​src/​cli/​services/​app-security-engine/​tests/​rule-analysis.test.ts Updates scan-context fixtures.
packages/​app/​src/​cli/​services/​app-security-engine/​tests/​react-router-detection.test.ts Covers flat, nested, included, and sibling-app layouts.
packages/​app/​src/​cli/​services/​app-security-engine/​scanners/​index.ts Integrates root discovery into scanning.
packages/​app/​src/​cli/​services/​app-security-engine/​rules/​types.ts Adds roots to the scan context.
packages/​app/​src/​cli/​services/​app-security-engine/​rules/​compliance-rules.ts Checks API versions across detected roots.
packages/​app/​src/​cli/​services/​app-security-engine/​capabilities/​detect.ts Implements root detection and ownership filtering.
.changeset/​app-security-react-router-roots.md Records the user-facing fix.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Once a React Router root outside the app directory was detected, the
framework-gated source checks read every gathered source file, so a sibling
app's routes could produce findings for this app. Source input for those checks
is now limited to the detected roots, leaving out other apps' code.

An app directory above this one was skipped entirely when excluding roots, so
a parent app's web directory could mark a nested app as React Router. Its own
root and its app and web directories are now left out, while shared packages
under it still count.

Co-authored-by: Qlaw <noreply@qlaw.quick.shopify.io>
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Differences in type declarations

We detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:

  • Some seemingly private modules might be re-exported through public modules.
  • If the branch is behind main you might see odd diffs, rebase main into this branch.

New type declarations

We found no new type declarations in this PR

Existing type declarations

packages/cli-kit/dist/public/node/system.d.ts
@@ -104,9 +104,9 @@ export declare function sleep(seconds: number): Promise<void>;
  */
 export declare function terminalSupportsHyperlinks(): boolean;
 /**
- * Check if standard input and standard error are terminals that support prompting.
+ * Check if the standard input and output streams support prompting.
  *
- * @returns True if standard input and standard error support prompting.
+ * @returns True if the standard input and output streams support prompting.
  */
 export declare function terminalSupportsPrompting(): boolean;
 /**
packages/cli-kit/dist/public/node/ui.d.ts
@@ -21,7 +21,6 @@ interface UIDebugOptions {
     skipTTYCheck?: boolean;
 }
 export interface RenderConcurrentOptions extends PartialBy<ConcurrentOutputProps, 'abortSignal'> {
-    /** Ink options for terminal UI. Finite JSON output uses the command event channel on stderr instead. */
     renderOptions?: RenderOptions;
 }
 /**
packages/cli-kit/dist/private/node/ui/components/ConcurrentOutput.d.ts
@@ -1,4 +1,4 @@
-import { type OutputProcess } from '../../../../public/node/output.js';
+import { OutputProcess } from '../../../../public/node/output.js';
 import { AbortSignal } from '../../../../public/node/abort.js';
 import { FunctionComponent } from 'react';
 export interface ConcurrentOutputProps {
@@ -6,21 +6,14 @@ export interface ConcurrentOutputProps {
     prefixColumnSize?: number;
     abortSignal: AbortSignal;
     showTimestamps?: boolean;
-    /**
-     * Keeps terminal UI running after all processes finish. Defaults to false.
-     * In JSON mode, false uses finite progress/diagnostic events; true retains streaming terminal UI.
-     */
     keepRunningAfterProcessesResolve?: boolean;
     useAlternativeColorPalette?: boolean;
 }
 interface ConcurrentOutputContext {
     outputPrefix?: string;
-    /** Controls ANSI stripping for terminal output. JSON diagnostics are always unstyled. */
     stripAnsi?: boolean;
 }
 declare function useConcurrentOutputContext<T>(context: ConcurrentOutputContext, callback: () => T): T;
-/** Runs finite processes concurrently and routes their output through the shared diagnostic context. */
-export declare function runConcurrentProcessesForJson({ processes, abortSignal, }: Pick<ConcurrentOutputProps, 'processes' | 'abortSignal'>): Promise<void>;
 /**
  * Renders output from concurrent processes to the terminal.
  * Output will be divided in a three column layout

jplhomer and others added 4 commits October 7, 2026 11:24
Windows CI compared ScanResult paths (cli-kit forward slashes) against
fileRealPath tmp dirs (native backslashes). Use normalizePath.

Refs shop/issues-develop#24160

Co-authored-by: Qlaw <noreply@qlaw.quick.shopify.io>
Skipped source inputs now use the same React Router root and other-app
predicate as inspected files, so a too-large or unreadable file in a
sibling app no longer leaves this app's React Router checks unresolved.

Refs shop/issues-develop#24160

Co-authored-by: Qlaw <noreply@qlaw.quick.shopify.io>
…irectory

An app configuration file inside a detected React Router root's app directory made that directory another app's, so source checks silently skipped the routes and server. Only exclude other-app directories that don't overlap a detected root's app directory.

Refs shop/issues-develop#24160

Co-authored-by: Qlaw <noreply@qlaw.quick.shopify.io>
An app configuration file in any subdirectory of a detected React Router root, such as lib/shopify.app.toml in a flat app, still made that directory another app's, so source checks and skipped-input accounting silently left it out. Only exclude other apps' code outside the detected roots: sibling apps and a parent app's app and web directories.

Refs shop/issues-develop#24160

Co-authored-by: Qlaw <noreply@qlaw.quick.shopify.io>

expect(result.detection).toMatchObject({framework: 'react_router', surface: 'react_router'})
expect(frameworkGatedChecks(result)).toEqual([])
expect(eolSourceFindings(result)).toEqual(['../../packages/server/app/shopify.server.ts'])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a case showing that all qualifying React Router roots attributed to the selected app are scanned, rather than only one?

i.e. for something like

   repository/
   ├── apps/
   │   └── product-reviews/
   │       └── shopify.app.toml
   └── packages/
       ├── server/
       └── server-next/

Add a test that scans both React Router servers attributed to one app. Bump EOL_API_VERSION, EXPIRING_OFFLINE_TOKEN, UNAUTHENTICATED_ENDPOINT, REQUEST_CONTROLLED_ADMIN_CONTEXT, DEPRECATED_SCRIPT_TAG_SCOPE, CREDENTIAL_LOG_LEAKAGE, CREDENTIAL_BROWSER_LEAKAGE, UNSAFE_INNERHTML and APP_PROXY_LIQUID_INJECTION, since React Router root detection changes which source they inspect.
# Conflicts:
#	packages/app/src/cli/services/app-security-engine/checks/embedded.ts
#	packages/app/src/cli/services/app-security-engine/tests/deterministic-rules.test.ts
A root above the app directory, such as ../.., prefixes paths like ../../../other-app/..., so containment now also rejects a remainder that climbs out of the root. This applies to source inclusion, other apps' code exclusion and root ownership.

@dmerand dmerand left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The agent had an edge case here worth looking at. Otherwise LGTM.

(directory) => !isInsideReactRouterRoot(directory, reactRouterRoots),
)
return (
reactRouterRoots.some((root) => pathWithinRoot(root, path) !== undefined) &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep gathered shared app code in the check inputs, or report its omitted coverage. With the selected TOML at the app root and only web/ detected, a route imports ../../../lib/preview, but lib/preview.ts is dropped here.

I reproduced this with a fresh build, the compiled engine, and the actual CLI: the helper is listed and read, but element.innerHTML = payload produces UNSAFE_INNERHTML v3 executed with zero findings and no coverage reason for this omission. Moving the same helper to web/lib/preview.ts produces one finding. Please add a regression test while retaining the sibling-app exclusion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated!

Code in the app directory belongs to this app even when it's outside every detected React Router root, such as a lib module that a route in web imports. React Router source inputs now include it, so checks like UNSAFE_INNERHTML inspect it. Other apps' code, including apps nested in the app directory, and paths that climb past a root stay out.
# Conflicts:
#	packages/app/src/cli/services/app-security-engine/tests/deterministic-rules.test.ts
…-monorepo-react-router-detection

# Conflicts:
#	packages/app/src/cli/services/app-security-engine/tests/deterministic-rules.test.ts
@jplhomer
jplhomer added this pull request to the merge queue Oct 9, 2026
jplhomer added a commit that referenced this pull request Oct 9, 2026
…ity check (#8827)

Backport of #8827 (from approved PR head 92afbdf; it was in the merge queue when backported)
Merged via the queue into main with commit a9f536b Oct 9, 2026
28 of 30 checks passed
@jplhomer
jplhomer deleted the jplhomer/app-security-monorepo-react-router-detection branch October 9, 2026 15:20
dmerand added a commit that referenced this pull request Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: @shopify/app @shopify/app package issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants