feat: guard edge deploy against out-of-scope suggestions for subpath sites#2595
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.
…silent failures Addresses PR #2595 review feedback: - Log a warning instead of silently disabling the scope guard when a site's baseURL is unparseable, so operators can detect ineffective tenant isolation. - allowedRegexPatterns entries are compiled as regexes and matched against the full URL downstream (buildUrlMatcher), so a plain startsWith() check on the pattern text was bypassable via regex alternation (e.g. `/kings/.*|/wolves/.*`). Patterns are now only trusted as in-scope when they exactly match the site's base path or use the `/*` prefix-match convention; anything else is rejected (fail closed). - Hoist the per-batch siteBasePath resolution out of the per-suggestion closure. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Suggestion URLs were scope-checked by pathname only, letting a suggestion for https://attacker.example/kings/page pass on a site scoped to https://example.com/kings. Route the URL-side check through isWithinSiteScope (host+port aware) and extract the pattern-side check into src/utils/path-scope.js.
…tils Bump @adobe/spacecat-shared-utils to 1.124.0, which now ships the hardened subpath scope guard, and drop the local src/utils/path-scope.js copy in favor of the shared implementation.
… feat/subpath-deploy-guards
npm install --package-lock-only had left the lockfile's root dependency entry for @adobe/spacecat-shared-http-utils on a caret range while package.json pins it exactly; reconcile after merging main.
There was a problem hiding this comment.
Hey @dhavkuma,
⚠ Degraded review - no spec document was found for this change (searched the PR links, the touched repos' docs, the architecture/guidelines docs, and linked Jira). This review covers code-level quality but could not validate the change against an agreed design, so confidence is reduced. Add a spec link (PR template section 4) and re-request review for a full-confidence pass.
Verdict: Approve - the scope guard is well-implemented, both prior findings are addressed, and no blocking issues remain.
Complexity: HIGH - medium diff; API surface + dependency signal.
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 (4 files).
Non-blocking (4): minor issues and suggestions
- nit: The inner try/catch for URL parsing could use
URL.canParse(url)(Node 20+) to avoid the try/catch/eslint-disable dance -src/controllers/suggestions.js:1849 - suggestion: Add one test where the URL comes from a
getSuggestionUrlfallback field (e.g.data.recommendations[0].pageUrl) and is out of scope, to prove the full URL-resolution chain feeds into the guard -test/controllers/suggestions.test.js - suggestion: Tighten acceptance-test assertions to verify
body.suggestions[0]is defined before asserting on.message, preventing false passes from undefined -test/controllers/suggestions.test.js - suggestion: Add a structured log field (the rejected URL or pattern) to the scope-guard warning at line 1876 to aid incident investigation -
src/controllers/suggestions.js:1876
Previously flagged, now resolved
- Unparseable site base URL now logs a warning before passing suggestions through
allowedRegexPatternsbypass via regex metacharacters now delegates toisPathPatternWithinSiteScopefrom shared-utils, which fails closed on any pattern containing metacharacters
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 1m 32s | Cost: $7.42 | Commit: c586411338c239473ba5119ca6a0682144d8b811
If this code review was useful, please react with 👍. Otherwise, react with 👎.
Hoist siteBaseURL to where apexBaseUrl is first derived and reuse it for the batch-level siteBasePath resolution, instead of calling site.getBaseURL() twice.
There was a problem hiding this comment.
Hey @dhavkuma,
⚠ Degraded review - no spec document was found for this change (searched the PR links, the touched repos' docs, the architecture/guidelines docs, and linked Jira). This review covers code-level quality but could not validate the change against an agreed design, so confidence is reduced. Add a spec link (PR template section 4) and re-request review for a full-confidence pass.
Verdict: Approve - the single new commit is a correct, trivial refactor with no behavioral change; no blocking issues remain.
Complexity: HIGH - medium diff; API surface + dependency signal.
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 (4 files).
Non-blocking (1): minor issues and suggestions
- suggestion: Consider adding a structured log field (the rejected URL or pattern) to the scope-guard warning at line 1876 to aid incident triage -
src/controllers/suggestions.js:1876
Previously flagged, now resolved
- Duplicate
site.getBaseURL()call now hoisted to a singlesiteBaseURLdeclaration reused by bothapexBaseUrlandsiteBasePath
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 5m 32s | Cost: $1.48 | Commit: b9d9ca1fb5e1f01ee978e9e8ca9f93b805ec8ff7
If this code review was useful, please react with 👍. Otherwise, react with 👎.
isSuggestionInScope now delegates entirely to isWithinSiteScope / isPathPatternWithinSiteScope, which handle root-level sites and fail closed on anything they cannot confirm to be in scope. Removes the dead siteBasePath pre-parse block and the redundant inline guards. Behavior changes (fail open -> fail closed): - unparseable site baseURL now rejects the batch instead of skipping the guard - a suggestion URL that resolves outside a subpath site's base path is rejected Preserved (fail open): a suggestion with no resolvable URL is let through (nothing to scope-check) so no-URL deploy + auto-cover keeps working on all sites, including root. Tests updated to assert the new fail-closed behavior; fixed the incomplete headingsOpportunity mock (missing getData) that previously only worked because root sites short-circuited before getSuggestionUrl. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Hey @dhavkuma,
⚠ Degraded review - no spec document was found for this change (searched the PR links, the touched repos' docs, the architecture/guidelines docs, and linked Jira). This review covers code-level quality but could not validate the change against an agreed design, so confidence is reduced. Add a spec link (PR template section 4) and re-request review for a full-confidence pass.
Verdict: Approve - the simplification commit is a correct, security-improving refactor with no blocking issues.
Complexity: HIGH - medium diff; API surface + dependency signal.
Changes: Simplifies the edge-deploy scope guard by removing local URL parsing and delegating entirely to shared-utils, changing behavior to fail-closed on unparseable inputs (2 files).
Non-blocking (2): minor issues and suggestions
- suggestion: Add a structured log field (the rejected URL or pattern) to the scope-guard warning so operators can distinguish "out of scope" from "could not parse" during incident triage -
src/controllers/suggestions.js:1849 - suggestion: Consider a defensive try/catch around the
isSuggestionInScopecall site so that a future shared-utils release changing error semantics cannot abort the entire batch-processing loop -src/controllers/suggestions.js:1848
Previously flagged, now resolved
- Local URL parsing and root-level site early-return logic removed; shared-utils now owns all scope decisions, eliminating duplicated parsing and strengthening the fail-closed posture
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 1m 4s | Cost: $3.98 | Commit: 3bf7d62e89f6b7123a5f48e526b205bb3864b710
If this code review was useful, please react with 👍. Otherwise, react with 👎.
…uards # Conflicts: # package-lock.json
Regression coverage for edge-deploy's subpath scope guard: proves domain-wide, path-level, and regular suggestions still classify and deploy correctly together (and in isolation from each other) on a /kings-style subpath site, across both the synchronous deployToEdge flow and the respond-async geo-experiment flow, which share the same isSuggestionInScope classification. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Hey @dhavkuma,
⚠ Degraded review - no spec document was found for this change (searched the PR links, the touched repos' docs, the architecture/guidelines docs, and linked Jira). This review covers code-level quality but could not validate the change against an agreed design, so confidence is reduced. Add a spec link (PR template section 4) and re-request review for a full-confidence pass.
Verdict: Approve - no blocking issues found; the new merge and test commits strengthen coverage without introducing concerns.
Complexity: HIGH - large diff; API surface + dependency signal.
Changes: Adds a merge from main and comprehensive tests for mixed domain-wide + path-level suggestion scoping on subpath sites (4 files).
Non-blocking (4): minor issues and suggestions
- suggestion: The "rejects a path suggestion" test uses
isDomainWide: falsewithout settingpathType/pathPattern, so it may exercise the URL branch rather than theisPathSuggestionbranch. AddpathType: 'prefix'to confirm it tests the intended path -test/controllers/suggestions.test.js:240 - suggestion: Add a test for an explicitly empty
allowedRegexPatterns: []on a subpath site to document the pass-through behavior -test/controllers/suggestions.test.js - suggestion: Assert
statusCodeon each individual suggestion in the sync regression "deploys all 3 together" test, matching the async test's assertion pattern -test/controllers/suggestions.test.js:632 - suggestion: Assert
tokowakaClientStub.deployToEdge.calledisfalsein the async experiment flow test to guard against fallthrough to sync deploy -test/controllers/suggestions.test.js:456
Previously flagged, now resolved
- Duplicate
site.getBaseURL()call hoisted to singlesiteBaseURLdeclaration
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 1m 28s | Cost: $3.75 | Commit: e392aa251b903ed92d5e0f23feeaec1def504881
If this code review was useful, please react with 👍. Otherwise, react with 👎.
# [1.621.0](v1.620.0...v1.621.0) (2026-07-03) ### Features * guard edge deploy against out-of-scope suggestions for subpath sites ([#2595](#2595)) ([f084db1](f084db1))
|
🎉 This PR is included in version 1.621.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
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