[5] CSP path matching bypassed via percent-encoded slashes
Rated Medium because the diff fixes a deterministic CSP path-restriction bypass exploitable when combined with a separate URL-injection primitive on the allowed origin; impact is policy bypass, not memory corruption.
WebKit's pathMatches() diverged from the CSP3 spec by percent-decoding the entire URL path as a flat string, then doing prefix/equality checks. This made it vulnerable to %2F..%2F path-traversal bypasses. The fix adopts the spec's algorithm (§6.7.2.12): split both paths on literal /, percent-decode each segment, and compare corresponding pairs.
Source/WebCore/page/csp/ContentSecurityPolicySource.cpp
Source/WebCore/page/csp/ContentSecurityPolicySourceList.cpp
Patch Details
The old implementation percent-decoded the entire URL path as one flat string and then did a startsWith/equality check against m_path. The new implementation splits both m_path and url.path() on the literal / via splitAllowingEmptyEntries('/'), applies steps 1–7 of the spec (empty-path fast path, / vs empty, directory-vs-exact match, segment count comparison, trailing empty-segment removal), and only then percent-decodes each segment in isolation before comparing. Complementarily, parsePath() no longer percent-decodes the directive's path at parse time — m_path now retains raw %2F sequences so the segment split is meaningful.
Decode-before-tokenise ordering in a path matcher: percent-decoding the full URL path before splitting on '/' lets encoded slashes (%2F) cross structural segment boundaries that the access-control policy is defined over.
Background
A CSP directive carries a list of source expressions; each has a scheme, host, port and optional path component. URL fetching consults ContentSecurityPolicySource::matches(), which checks scheme/host/port/path. CSP3 §6.7.2.12 defines path matching as a segment-wise operation — both paths are split on literal /, each pair of segments is percent-decoded, and the decoded segments are compared. A directive ending in / is a directory match; otherwise it is an exact match. %2F is the percent-encoding of /, and the URL standard preserves %2F literally in the path component rather than decoding it to a structural separator, so /a%2Fb/c has two segments (a%2Fb and c), not three. PAL::decodeURLEscapeSequences has no notion of segment boundaries and turns %2F into a literal /.
Analysis
pathMatches() performed the comparison on a fully percent-decoded URL path. PAL::decodeURLEscapeSequences(url.path()) turns %2F into a literal /, so an attacker URL like http://host/security/contentSecurityPolicy%2F..%2F..%2Fresources/script.js decodes to /security/contentSecurityPolicy/../../resources/script.js. The decoded string starts with /security/contentSecurityPolicy/, which startsWith(m_path) accepts — even though the URL's true path segment is the single literal blob contentSecurityPolicy%2F..%2F..%2Fresources that does NOT live under /security/contentSecurityPolicy/resources/ per the CSP3 segment-wise definition.
An attacker who controls a URL string fetched by the page — for instance via an HTML-injection primitive that lets them write a <script src=...> attribute, an open redirect on the allowed origin, or a server-side reflection sink — can craft the path so its first un-encoded segment matches the policy's allowed prefix while subsequent encoded-slash sequences smuggle in a different effective path. The seven test cases give an exact recipe: with directive script-src host/security/contentSecurityPolicy/resources/, the URL host/security/contentSecurityPolicy/resources%2F..%2F..%2Fresources/script.js was accepted pre-fix because the decoded string passed startsWith; the real second segment is the literal resources%2F..%2F..%2Fresources, which does not equal resources.
This vulnerability weakened the CSP path-restriction trust boundary. CSP3 assumes a directive like script-src https://example.com/safe/ restricts script loads to URLs whose path segments live under /safe/; before the fix, an attacker who could influence a URL via an open redirect, reflected src injection, or any other XSS sink could use %2F..%2F to escape the directory restriction and load scripts from anywhere on the same origin. CSP path matching is the second line of defence, not the first; the attacker still needs origin reachability through the directive and some primitive to introduce the URL string. The decode-then-compare anti-pattern has produced bypasses in URL parsers, web servers, and proxies for two decades — the CSP3 spec text is unusually explicit about segment-then-decode ordering precisely because the inverse ordering is the default, intuitive implementation.
Audit directions
- Decode-then-compare in URL/path access-control checks. Audit any policy or routing code that calls
decodeURLEscapeSequences(or any percent-decode helper) on a full URL component before tokenising or comparing it against an allowlist. Within WebKit, grepSource/WebCoreandSource/WebKitfor callers ofPAL::decodeURLEscapeSequenceswhose argument is a full path/query/fragment and whose result is then split or prefix-compared. Likely targets include same-origin checks on path-restricted features, service-worker scope matching, and extension manifest origin/path matching. - URL-component allowlists compared without consulting the relevant spec's tokenisation order. Verify other CSP3 matchers (host wildcards via
hostMatches, port matching, scheme matching) and similar policy systems (Permissions-Policy, Trusted Types, Reporting API endpoints, Fetch metadata) split-then-decode rather than decode-then-split. Start withContentSecurityPolicySource::hostMatchesandwildcardMatchesto confirm host-side comparisons are not similarly vulnerable to percent-encoded.(%2E). - Differential decoding between policy-time storage and check-time comparison. The
parsePathchange shows that an eager decode at parse time can desync the stored form from the live URL form. Audit other CSP and security-policy parsers (parseHost,parsePort, source-list nonce/hash parsing, ReferrerPolicy, COEP/COOP) for similar eager normalisation that could create asymmetry with the runtime check. - Service-worker scope and other prefix-based URL gates. Service-worker scope matching uses a path-prefix rule that historically has shipped its own
%2Fbypass variants in other browsers. InspectSource/WebCore/workers/servicescope-matching code for the same decode-order anti-pattern.