feat: guard edge deploy against out-of-scope suggestions for subpath sites#2595
feat: guard edge deploy against out-of-scope suggestions for subpath sites#2595dhavkuma wants to merge 5 commits into
Conversation
…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>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
This PR will trigger a minor release when merged. |
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>
|
Fixed in ae2ee15. Both Added a regression test covering the prefix-collision case ( |
There was a problem hiding this comment.
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
- [Important] Missing observability when site base URL is unparseable - scope guard silently disabled -
src/controllers/suggestions.js:1800(details inline) - [Important]
allowedRegexPatternsstring prefix check may be bypassable via regex metacharacters -src/controllers/suggestions.js:1814(details inline)
Non-blocking (3): minor issues and suggestions
- nit:
isSuggestionInScopere-parsessite.getBaseURL()on every suggestion in the batch; thesiteBasePathis constant and could be hoisted above theforEach-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 theevery()boundary condition -test/controllers/suggestions.test.js - suggestion: Add a test for
isPathSuggestiontype (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 { |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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.
Summary
isSuggestionInScope()insidedeploySuggestionToEdgeinsrc/controllers/suggestions.jsnba.com/kings), suggestions whose URL orallowedRegexPatternsfall outside the site's registered base path are rejected with a400entry in the 207 response — preventing cross-tenant CDN deployments (e.g. anba.com/kingssite cannot accidentally deploy suggestions scoped tonba.com/wolves)pathname === '/') pass all suggestions through unchanged, so there is no regression for existing deploymentsLogic
new URL(suggestion.url).pathname.startsWith(siteBasePath)allowedRegexPatternsstarts withsiteBasePathRelated PRs
Test plan
test/controllers/suggestions.test.jsundersubpath scope guarddescribe blockdeploySuggestionToEdgetests still passnpm test)"outside the scope"messageallowedRegexPatterns→ 207 with"outside the scope"message🤖 Generated with Claude Code