[5] CSP path matching bypassed via percent-encoded slashes
diff가 수정하는 내용은 허용된 origin에서 별도의 URL injection primitive와 결합 시 매번 재현 가능한 CSP 경로 제한 우회입니다. 영향 범위는 policy bypass에 국한되며, memory corruption은 아닙니다. 이 때문에 Medium으로 평가합니다.
WebKit의 pathMatches()는 CSP3 명세와 달리 동작했습니다. URL path 전체를 단일 flat string으로 percent-decode한 뒤 prefix/equality 비교를 수행하는 방식이었으며, 이 구조는 %2F..%2F를 이용한 path traversal 우회에 취약했습니다. 수정된 구현은 명세의 알고리즘(§6.7.2.12)을 채택합니다. 두 경로를 모두 literal /로 분리하고, 각 segment를 개별적으로 percent-decode한 뒤 대응하는 쌍을 비교합니다.
Source/WebCore/page/csp/ContentSecurityPolicySource.cpp
Source/WebCore/page/csp/ContentSecurityPolicySourceList.cpp
Patch Details
기존 구현은 URL path 전체를 하나의 flat string으로 percent-decode한 뒤 m_path에 대해 startsWith/equality 비교를 수행했습니다. 새 구현은 m_path와 url.path() 모두를 splitAllowingEmptyEntries('/')를 통해 literal / 기준으로 분리합니다. 이후 명세의 1~7단계를 적용하는데, 빈 path fast path, / vs empty 처리, directory/exact match 판별, segment 개수 비교, 후행 빈 segment 제거가 이에 해당합니다. 비교 직전에만 각 segment를 개별적으로 percent-decode합니다. 보완적으로, parsePath()는 더 이상 parse 시점에 directive의 path를 percent-decode하지 않습니다. m_path는 이제 raw %2F 시퀀스를 그대로 유지하므로, segment 분리가 의미 있게 동작합니다.
Path matcher에서의 decode-before-tokenise 순서 문제: URL path 전체를 '/'로 분리하기 전에 percent-decode하면, 인코딩된 슬래시(%2F)가 access-control policy가 정의하는 구조적 segment 경계를 넘어설 수 있습니다.
Background
CSP directive는 source expression 목록을 포함하며, 각 expression은 scheme, host, port, 그리고 선택적 path 구성요소를 갖습니다. URL fetching 과정에서는 ContentSecurityPolicySource::matches()가 호출되어 scheme, host, port, path를 순서대로 검사합니다. CSP3 §6.7.2.12는 path matching을 segment 단위 연산으로 정의합니다. 두 경로를 모두 literal /로 분리하고 각 segment 쌍을 percent-decode한 뒤 비교하는 방식입니다. /로 끝나는 directive는 directory match, 그렇지 않으면 exact match로 구분합니다.
%2F는 /의 percent-encoding입니다. URL 표준은 path 구성요소에서 %2F를 구조적 구분자로 해석하지 않고 그대로 유지합니다. 따라서 /a%2Fb/c는 세 개가 아닌 두 개의 segment(a%2Fb와 c)를 갖습니다. 반면 PAL::decodeURLEscapeSequences는 segment 경계를 인식하지 못하고 %2F를 그대로 literal /로 변환합니다.
Analysis
pathMatches()는 URL path를 전체적으로 percent-decode한 상태에서 비교를 수행했습니다. PAL::decodeURLEscapeSequences(url.path())는 %2F를 literal /로 변환합니다. 예를 들어 공격자가 http://host/security/contentSecurityPolicy%2F..%2F..%2Fresources/script.js와 같은 URL을 사용하면, 이는 /security/contentSecurityPolicy/../../resources/script.js로 decode됩니다. decode된 문자열은 /security/contentSecurityPolicy/로 시작하므로 startsWith(m_path)를 통과합니다. 그러나 CSP3의 segment 단위 정의에 따르면, URL의 실제 path segment는 단일 literal blob인 contentSecurityPolicy%2F..%2F..%2Fresources이며, 이는 /security/contentSecurityPolicy/resources/ 하위에 위치하지 않습니다.
페이지가 fetch하는 URL 문자열을 공격자가 제어할 수 있는 상황을 가정합니다. 예를 들어 <script src=...> 속성을 작성할 수 있는 HTML injection primitive, 허용된 origin의 open redirect, server-side reflection sink 등이 이에 해당합니다. 이런 조건에서 공격자는 경로를 교묘하게 조작할 수 있습니다. 첫 번째 un-encoded segment를 policy의 허용 prefix와 일치시키면서, 이후 encoded-slash 시퀀스로 실제로는 다른 유효 경로를 삽입하는 방식입니다.
7개의 테스트 케이스가 구체적인 재현 방법을 제시합니다. script-src host/security/contentSecurityPolicy/resources/ directive가 적용된 상태에서, host/security/contentSecurityPolicy/resources%2F..%2F..%2Fresources/script.js URL은 패치 이전에 허용되었습니다. decode된 문자열이 startsWith를 통과했기 때문입니다. 실제 두 번째 segment는 literal resources%2F..%2F..%2Fresources이며, 이는 resources와 동일하지 않습니다.
이 vulnerability는 CSP 경로 제한의 신뢰 경계를 약화시켰습니다. CSP3는 script-src https://example.com/safe/와 같은 directive가 /safe/ 하위 경로의 script 로드만을 허용한다고 가정합니다. 패치 이전에는 open redirect, reflected src injection, 기타 XSS sink를 통해 URL에 영향을 미칠 수 있는 공격자가 이 취약점을 활용할 수 있었습니다. %2F..%2F를 이용하면 directory 제한을 벗어나 같은 origin의 임의 위치에서 script를 로드하는 것이 가능했습니다. CSP path matching은 첫 번째 방어선이 아니라 두 번째 방어선입니다. 공격자는 여전히 directive를 통한 origin 도달 가능성과, URL 문자열을 삽입할 수 있는 primitive가 필요합니다. decode-then-compare anti-pattern은 URL parser, 웹 서버, proxy에서 20년에 걸쳐 우회 사례를 반복적으로 낳아 왔습니다. CSP3 명세가 segment-then-decode 순서를 유독 명시적으로 규정하는 이유는, 반대 순서가 대부분의 개발자가 직관적으로 선택하는 구현 방식이기 때문입니다.
Audit directions
- URL/path access-control 검사에서의 decode-then-compare 패턴. allowlist 비교나 토큰화 이전에 전체 URL 구성요소에 대해
decodeURLEscapeSequences(또는 유사한 percent-decode 함수)를 호출하는 policy/routing 코드를 점검해야 합니다. WebKit 내에서는Source/WebCore와Source/WebKit를 대상으로PAL::decodeURLEscapeSequences호출 지점을 검색해야 합니다. 인자가 전체 path/query/fragment이고 그 결과를 분리하거나 prefix 비교에 사용하는 경우를 집중적으로 살펴볼 필요가 있습니다. 주요 점검 대상으로는 path-restricted 기능의 same-origin 검사, service-worker scope matching, extension manifest origin/path matching 등이 있습니다. - 명세의 토큰화 순서를 고려하지 않은 URL 구성요소 allowlist 비교. 다른 CSP3 matcher(
hostMatches를 통한 host wildcard, port matching, scheme matching)가 split-then-decode 방식을 따르는지 확인해야 합니다. Permissions-Policy, Trusted Types, Reporting API endpoint, Fetch metadata 등 유사한 policy 시스템도 동일하게 점검할 필요가 있습니다. 먼저ContentSecurityPolicySource::hostMatches와wildcardMatches를 검토하여, host 측 비교가 percent-encoded.(%2E)에 대해 유사한 취약점을 갖지 않는지 확인해야 합니다. - policy 저장 시점과 검사 시점 간의 decode 불일치.
parsePath변경은 parse 시점의 조기 decode가 저장된 형태와 실제 URL 형태 사이에 불일치를 만들 수 있음을 보여줍니다. 다른 CSP 및 security-policy parser에서도 동일한 문제가 있는지 점검해야 합니다. 대상은parseHost,parsePort, source-list nonce/hash parsing, ReferrerPolicy, COEP/COOP이며, runtime 검사와 불일치를 유발할 수 있는 조기 정규화가 있는지 확인합니다. - Service-worker scope 및 기타 prefix 기반 URL 게이트. Service-worker scope matching은 path-prefix 규칙을 사용하며, 과거 다른 브라우저에서 자체적인
%2F우회 변형을 낳은 사례가 있습니다.Source/WebCore/workers/service의 scope-matching 코드에서 동일한 decode 순서 anti-pattern이 있는지 살펴볼 필요가 있습니다.