Skip to content

feat: guard edge deploy against out-of-scope suggestions for subpath sites#2595

Open
dhavkuma wants to merge 5 commits into
mainfrom
feat/subpath-deploy-guards
Open

feat: guard edge deploy against out-of-scope suggestions for subpath sites#2595
dhavkuma wants to merge 5 commits into
mainfrom
feat/subpath-deploy-guards

Conversation

@dhavkuma

Copy link
Copy Markdown

Summary

  • Adds isSuggestionInScope() inside deploySuggestionToEdge in src/controllers/suggestions.js
  • For subpath sites (e.g. nba.com/kings), suggestions whose URL or allowedRegexPatterns fall outside the site's registered base path are rejected with a 400 entry in the 207 response — preventing cross-tenant CDN deployments (e.g. a nba.com/kings site cannot accidentally deploy suggestions scoped to nba.com/wolves)
  • Root-level sites (pathname === '/') pass all suggestions through unchanged, so there is no regression for existing deployments

Logic

Suggestion type Guard checks
Regular new URL(suggestion.url).pathname.startsWith(siteBasePath)
Domain-wide / path Every entry in allowedRegexPatterns starts with siteBasePath
Root-level site Always passes through

Related PRs

Test plan

  • 4 new unit tests added to test/controllers/suggestions.test.js under subpath scope guard describe block
  • All existing deploySuggestionToEdge tests still pass
  • Full test suite passes (npm test)
  • Test: subpath site rejects regular suggestion with URL on sibling subpath → 207 with "outside the scope" message
  • Test: subpath site rejects domain-wide suggestion with wrong allowedRegexPatterns → 207 with "outside the scope" message
  • Test: subpath site accepts in-scope suggestion → passes scope guard, proceeds normally
  • Test: root-level site — all suggestions pass through unchanged

🤖 Generated with Claude Code

…sites

Adds isSuggestionInScope() to deploySuggestionToEdge so that suggestions
whose URL or allowedRegexPatterns fall outside the site's registered base
path are rejected with a 400 in the 207 response, preventing cross-tenant
CDN deployments (e.g. a nba.com/kings site cannot deploy wolves suggestions).

Root-level sites (pathname '/') pass all suggestions through unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@dhavkuma dhavkuma marked this pull request as draft June 11, 2026 17:04
@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@dhavkuma dhavkuma requested a review from MysticatBot June 11, 2026 17:37
@dhavkuma dhavkuma changed the base branch from main to fix/subpath-staging-domain-validation June 11, 2026 17:37
@dhavkuma dhavkuma changed the base branch from fix/subpath-staging-domain-validation to main June 11, 2026 17:38
@dhavkuma dhavkuma marked this pull request as ready for review June 11, 2026 17:41
@github-actions

Copy link
Copy Markdown

This PR will trigger a minor release when merged.

dhavkuma and others added 2 commits June 11, 2026 23:41
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`startsWith("/kings")` incorrectly passes `/kingston/page`.
Require the path to equal siteBasePath exactly or start with
`siteBasePath + "/"` — applies to both the URL pathname check
and the allowedRegexPatterns check.

Adds a regression test for the prefix-collision edge case.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@dhavkuma

Copy link
Copy Markdown
Author

Fixed in ae2ee15.

Both startsWith(siteBasePath) checks now require a path-segment boundary — the path must either equal siteBasePath exactly or start with siteBasePath + "/". This prevents /kings from matching /kingston/page.

Added a regression test covering the prefix-collision case (/kings site, URL with /kingston/page path → rejected).

@dhavkuma dhavkuma requested review from MysticatBot and removed request for MysticatBot June 16, 2026 06:16

@MysticatBot MysticatBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey @dhavkuma,

Verdict: Request changes - two observability/security gaps in the scope guard warrant attention before merge.
Changes: Adds a subpath scope guard to deploySuggestionToEdge that rejects suggestions targeting URLs outside the site's registered base path, preventing cross-tenant CDN deployments (2 files).

Must fix before merge

  1. [Important] Missing observability when site base URL is unparseable - scope guard silently disabled - src/controllers/suggestions.js:1800 (details inline)
  2. [Important] allowedRegexPatterns string prefix check may be bypassable via regex metacharacters - src/controllers/suggestions.js:1814 (details inline)
Non-blocking (3): minor issues and suggestions
  • nit: isSuggestionInScope re-parses site.getBaseURL() on every suggestion in the batch; the siteBasePath is constant and could be hoisted above the forEach - src/controllers/suggestions.js:1795
  • suggestion: Add a test with mixed in-scope/out-of-scope allowedRegexPatterns (e.g. ['/kings/page', '/wolves/page']) to exercise the every() boundary condition - test/controllers/suggestions.test.js
  • suggestion: Add a test for isPathSuggestion type (not domain-wide) with out-of-scope patterns to cover both branches of the || - test/controllers/suggestions.test.js

Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 2m 14s | Cost: $2.70 | Commit: 1e7b0efc1dcb2cdf2fd1b544fc88ec6e9f2f161e
If this code review was useful, please react with 👍. Otherwise, react with 👎.

let siteBasePath;
try {
siteBasePath = new URL(siteBaseURL).pathname;
} catch {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (blocking): This catch returns true without logging. If site.getBaseURL() returns malformed data, the scope guard is entirely disabled for that site with zero observability. This is a tenant isolation guard - silent degradation means operators cannot detect when it is ineffective for a given site.

Add a warning log before the pass-through:

} catch {
  context.log.warn(`[edge-deploy] site ${apexBaseUrl} has unparseable baseURL, skipping scope guard`);
  return true;
}

return true;
}
const scopePrefix = `${siteBasePath}/`;
return patterns.every((p) => p === siteBasePath || p.startsWith(scopePrefix));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (blocking): This checks p.startsWith(scopePrefix) treating allowedRegexPatterns values as literal path strings. If these are actually evaluated as regexes downstream (as the field name implies), a pattern like /kings/.*|/wolves/.* passes the prefix check (starts with /kings/) but when evaluated as a regex also matches /wolves/ paths - bypassing tenant isolation.

If patterns can contain regex metacharacters in practice, consider rejecting patterns containing scope-extending operators (|, (, lookahead syntax). If patterns are always simple path prefixes despite the field name, add a brief comment documenting that assumption so future maintainers know the string check is intentional.

@MysticatBot MysticatBot added the ai-reviewed Reviewed by AI label Jun 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-reviewed Reviewed by AI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants