Propagate CSP directives from the creating document to WorkletGlobalScope
CVE: CVE-2026-43670 · Safari 26.5 · Released May 13, 2026 Impact: Processing maliciously crafted web content may bypass Content Security Policy Apple's description: A Content Security Policy bypass was addressed with improved enforcement in AudioWorklet contexts. Credit: lebr0nli of National Yang Ming Chiao Tung University, Security and Systems Lab
Medium 등급이며, 상한선은 버그 자체가 아니라 해당 페이지가 원래 걸어둔 정책이 결정합니다. 기본 생성된 정책 객체는 모든 질의에 허용으로 응답하므로, worklet realm 안에서 CSP는 사실상 통제 수단으로 동작하지 않았습니다. memory-safety 요소는 없으며, 공격자가 이미 확보하지 못한 발판을 새로 얻게 되는 것도 아닙니다.
Content Security Policy는 document 단위로 기술되지만, 페이지가 스크립트를 실행할 수 있는 곳은 document 하나뿐이 아닙니다. 페이지가 addModule()로 띄우는 worklet은 모두 독립적인 스크립트 실행 컨텍스트입니다. AudioWorklet 뒤의 오디오 렌더링 realm, PaintWorklet 뒤의 paint callback realm이 여기에 해당하며, 각각 자신의 ContentSecurityPolicy 객체를 갖습니다. CSP3는 그 객체가 무엇을 담아야 하는지 명시하고 있습니다. 소유 document가 생성했고 그와 same-origin인 worklet global scope는 생성자의 policy list를 상속해야 하며, 모듈 fetch는 script-src의 통제를 받아야 합니다. 이번 commit 이전 WebKit의 worklet에서는 두 조건 모두 지켜지지 않았습니다.
관전 포인트: 엄격한 정책이 걸린 페이지에서 스크립트 실행 발판을 잡은 뒤, 아무런 강제도 이루어지지 않는 audio worklet으로 자리를 옮기는 흐름을 생각해 볼 수 있습니다. 그 안에서는 eval()이 제약 없이 실행되고, 페이지의 script-src가 금지한 원격 스크립트도 로드되며, 결과는 message port를 통해 페이지로 되돌아옵니다.
Source/WebCore/workers/WorkerGlobalScope.cpp
Source/WebCore/workers/WorkerOrWorkletGlobalScope.cpp
Source/WebCore/worklets/WorkletParameters.h
Source/WebCore/Modules/webaudio/AudioWorkletMessagingProxy.cpp
Source/WebCore/Modules/webaudio/AudioWorkletGlobalScope.cpp
Source/WebCore/worklets/PaintWorkletGlobalScope.cpp
Source/WebCore/loader/FetchOptions.h
Source/WebCore/bindings/js/WorkerModuleScriptLoader.cpp
Source/WebKit/NetworkProcess/NetworkLoadChecker.cpp
Patch Details
서로 거의 겹치지 않는 두 개의 파일 묶음에서, 독립적인 enforcement 누락 두 건이 함께 수정되었습니다.
첫 번째 묶음은 policy 상속입니다. applyContentSecurityPolicyResponseHeaders()는 세 줄짜리 wrapper로, ContentSecurityPolicy::didReceiveHeaders()를 호출해 header snapshot을 해당 context의 실제 policy 객체에 반영합니다. 이 함수가 WorkerGlobalScope에서 삭제되고, 공통 base인 WorkerOrWorkletGlobalScope에 동일한 형태로 다시 선언되었습니다. forward declaration 역시 두 헤더 사이를 이동했습니다. WorkletParameters에는 ContentSecurityPolicyResponseHeaders contentSecurityPolicyResponseHeaders 멤버가 추가되었는데, 두 개의 isolatedCopy() overload에 모두 반영되어 worklet thread로 넘어가는 과정에서도 값이 유지됩니다. AudioWorkletMessagingProxy::generateWorkletParameters()가 owner document에서 이 값을 채우고, AudioWorkletGlobalScope::tryCreate()는 addToContextsMap() 직후에 이를 적용합니다. PaintWorkletGlobalScope::tryCreate()에도 같은 호출이 추가되었습니다. 다만 이쪽은 main thread에서 생성되므로 직렬화된 snapshot이 필요 없고, Document에서 바로 읽어옵니다.
두 번째 묶음은 worklet module fetch에 대한 directive 선택이며, process 경계 양쪽 모두에서 수정되었습니다. FetchOptions.h에서는 isScriptLikeDestination() 안에 있던 두 worklet destination을 따로 떼어내어 isWorkletDestination()이라는 이름의 predicate로 분리했습니다. WebContent 쪽 검사인 WorkerModuleScriptLoader::load()는 script-src 분기 조건을 destination == Script에서 Script || isWorkletDestination(...)으로 넓혔습니다. Network 쪽 검사인 NetworkLoadChecker::isAllowedByContentSecurityPolicy()는 redirect가 발생할 때마다 다시 수행되는데, 여기서는 Audioworklet/Paintworklet case가 기존에 fall through로 합류하던 worker 그룹보다 앞에서 script-src 검사 결과를 반환하도록 바뀌었습니다.
Before: After:
destination == Script ? Script || isWorkletDestination() ?
├ yes → allowScriptFromSource() ├ yes → allowScriptFromSource() [script-src]
└ no → allowWorkerFromSource() └ no → allowWorkerFromSource() [worker-src]
▲ ▲
└─ Audioworklet, Paintworklet └─ Worker, Serviceworker, Sharedworker
부수 변경도 있습니다. worklet processor script를 포함한 새 LayoutTest 두 개(audioworklet-inherits-allows-eval, audioworklet-inherits-blocks-eval)가 추가되었습니다. import된 WPT worklets/audio-worklet-csp.https expectation에서는 subtest 세 개가 FAIL → PASS로 바뀌었습니다. 반대로 GLib 플랫폼의 script-src-self/worklet-audio.https.html expectation 두 개는 Pass → Failure로 이동하면서 GTK/WPE port의 후속 작업으로 남았습니다.
Background
Content Security Policy. CSP는 document 단위로 적용되는 policy로, Content-Security-Policy response header나 <meta http-equiv> 태그를 통해 전달됩니다. 어떤 source가 script를 제공할 수 있는지, 그리고 런타임에 문자열을 코드로 바꾸는 동작이 허용되는지를 제한합니다. script 계열 load를 담당하는 directive가 script-src이며, 그 안의 'unsafe-eval' 토큰이 eval()과 new Function()을 허용하는 역할을 맡습니다. worker load는 worker-src가 담당합니다. 각 directive는 명시되지 않았을 때 적용되는 fallback chain을 spec 차원에서 따로 갖습니다. 그래서 둘 중 하나만 선언한 페이지에서도 두 directive를 서로 바꿔 쓸 수 없습니다.
eval 차단 지점. WebCore에서 script를 실행하는 context는 모두 ContentSecurityPolicy 객체를 하나씩 보유합니다. JSC는 문자열을 코드로 평가하기 전에 context를 거쳐 이 policy를 조회하며, policy가 거부한 평가는 script 쪽에 EvalError 예외로 나타납니다.
ContentSecurityPolicyResponseHeaders. policy를 직렬화한 snapshot에 해당합니다. 파싱된 directive 구조가 아니라 raw header 문자열을 담습니다. 전달용 형태라고 볼 수 있는데, ContentSecurityPolicy::didReceiveHeaders()가 snapshot을 실제 policy 객체에 반영하고, 타입 자체가 isolatedCopy()를 제공하므로 다른 thread로 안전하게 전달할 수 있습니다.
Worklet. AudioWorklet과 PaintWorklet은 addModule(url)로 가져온 작성자 제공 ES module을 실행하는 script context입니다. 이 중 audio worklet은 별도의 real-time audio thread에서 동작합니다. 작성자 코드는 AudioWorkletProcessor를 상속한 뒤 registerProcessor()로 이름을 붙여 등록합니다. 페이지에서 AudioWorkletNode(context, name)을 생성하면 해당 processor가 worklet thread에서 인스턴스화되며, 각 processor는 this.port에 MessagePort를 갖고 있어 document 쪽으로 구조화된 메시지를 전달할 수 있습니다.
global scope 계층 구조. 공통 base는 WorkerOrWorkletGlobalScope입니다. 한쪽 갈래에는 WorkerGlobalScope(dedicated, shared, service worker)가 있고, 반대쪽 갈래에는 WorkletGlobalScope가 있으며 그 아래에 AudioWorkletGlobalScope와 PaintWorkletGlobalScope가 위치합니다. 한쪽 subtree에 protected로 선언된 멤버는 반대쪽 subtree에서 접근할 수 없습니다.
WorkletParameters. worklet 생성 시점에 전달되는 main thread 상태를 모아둔 구조체입니다. window URL, runtime flag, sample rate, session ID, settings, referrer policy, agent cluster ID 등이 들어갑니다. 두 개의 isolatedCopy() overload는 brace initialization으로 모든 멤버를 위치 순서에 따라 deep copy합니다. 즉 인자 목록과 멤버 목록이 같은 struct를 설명하는 두 벌의 기술이며, 양쪽 모두 손으로 관리해야 합니다.
Fetch destination과 directive 선택. FetchOptions::Destination은 해당 load가 무엇을 위한 것인지 나타내는 태그로, Script, Worker, Serviceworker, Audioworklet, Paintworklet 등이 있고 종류는 계속 늘어나고 있습니다. CSP는 destination을 directive에 대응시키며, WebKit은 두 계열을 서로 다른 진입점으로 구현했습니다. ContentSecurityPolicy::allowScriptFromSource()는 script-src를, allowWorkerFromSource()는 worker-src를 조회하고, 각자 자신의 fallback 규칙을 적용합니다.
load가 검사되는 지점. worker와 worklet context의 module load는 WebContent process의 WorkerModuleScriptLoader::load()를 거칩니다. 이 함수는 판단을 내리는 동시에 request에 ContentSecurityPolicyEnforcement 값을 기록합니다. 한편 Network process의 NetworkLoadChecker는 같은 종류의 검사를 독립적으로 수행하며, redirect가 발생할 때마다 다시 검증합니다. 이번 commit이 수정한 switch 문의 redirectResponseReceived와 preRedirectURL 인자가 그 재검증에 사용됩니다.
Analysis
첫 번째 결함은 policy 상속이 이루어지지 않은 문제이며, 그 끝에는 허용적인 기본값이 놓여 있었습니다. 자식 context가 자신을 생성한 쪽 policy의 사본이 아니라 빈 policy를 가진 채 만들어졌기 때문입니다.
Document CSP ─┬─ headers in WorkerParameters
│ └──────────────► WorkerGlobalScope
│ └─ applyCSP…ResponseHeaders() ✓ policy replayed
│
└─ (no field, no call)
└──────X───────► WorkletGlobalScope
└─ ContentSecurityPolicy{} ✗ empty → allows all
다이어그램의 두 갈래는 모두 실제 ContentSecurityPolicy 객체로 끝납니다. 양쪽 다 ScriptExecutionContext이고, 그런 context는 예외 없이 policy 객체를 하나 보유하기 때문입니다. 차이는 그 객체에 무엇이 채워졌는가에 있습니다. worker 쪽에서는 parameters struct가 header snapshot을 함께 실어 날랐고, WorkerGlobalScope가 이를 policy에 반영했습니다. 반면 worklet 쪽에는 그것을 실어 나를 수단 자체가 없었습니다. WorkletParameters에 CSP 필드가 존재하지 않았기 때문입니다. 설령 필드가 있었더라도 호출할 함수가 없었습니다. applyContentSecurityPolicyResponseHeaders()가 형제 subtree에 protected로 선언되어 있어, AudioWorkletGlobalScope와 PaintWorkletGlobalScope에서는 보이지 않았기 때문입니다. 결과적으로 worklet realm이 갖게 된 것은 default 생성된 policy였습니다. directive를 하나도 담고 있지 않은 policy는 모든 질의에 허용으로 응답합니다. 문자열이 코드로 바뀌는 것을 막으려고 페이지가 명시한 script-src 통제가, 그 realm 안에서는 아무 역할도 하지 못한 셈입니다.
새로 추가된 audioworklet-inherits-blocks-eval 테스트는 수정된 동작을 정확히 그 지점에 고정합니다. 유발 과정은 그대로 PoC로 쓸 수 있을 만큼 짧습니다:
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'">가 걸린 document를 제공합니다.'unsafe-eval'이 빠져 있다는 점이 핵심입니다.new OfflineAudioContext(2, 100, 44100)을 만든 뒤context.audioWorklet.addModule("…/audioworklet-inherits-blocks-eval.js")를 호출합니다.- 해당 module은 constructor에서
try블록 안으로eval("1 + 0")을 호출하는 processor를 등록합니다. - 페이지에서
new AudioWorkletNode(context, 'eval-test')를 호출하면 그 processor가 worklet thread에서 생성됩니다. - processor는
this.port를 통해 결과를 document로 전달합니다.
class EvalTestProcessor extends AudioWorkletProcessor {
constructor() {
super();
var exception;
try { eval("1 + 0"); } catch (e) { exception = e; }
if (!exception)
this.port.postMessage("FAIL should throw EvalError. But did not throw an exception.");
else if (exception instanceof EvalError)
this.port.postMessage("PASS threw exception " + exception + ".");
…
영향 범위를 따진다면 5번 단계를 눈여겨볼 필요가 있습니다. worklet은 attacker가 격리되어 갇히는 sandbox가 아닙니다. document로 곧장 이어지는 구조화된 채널을 가진 same-origin realm입니다. 그래서 그곳에서 실행된 코드는, 스스로는 같은 결과를 만들 수 없는 페이지 script에게 그 결과를 전달할 수 있습니다.
두 번째 결함은 동작 방식은 전혀 다르지만 형태는 동일합니다. 누락으로 인해 허용 쪽으로 기울어진 분류입니다. WorkerModuleScriptLoader::load()는 Destination::Script인 경우에만 script-src를 적용하고 나머지는 전부 else로 넘겼습니다. 그 결과 worklet module load는 allowWorkerFromSource()로 검증되고 EnforceWorkerSrcDirective로 표시되었습니다. NetworkLoadChecker::isAllowedByContentSecurityPolicy()도 구조적으로 같은 오답에 도달했습니다. Audioworklet과 Paintworklet case에 본문이 없어 Worker/Serviceworker/Sharedworker 그룹으로 fall through했기 때문입니다. worker-src와 script-src는 fallback chain이 서로 다릅니다. 압도적으로 흔한 경우인 script-src만 선언한 document를 가정해 보겠습니다. 이 경우 worklet load는 작성자가 실제로 명시한 directive가 아니라, worker-src의 fallback이 우연히 해석해 낸 결과에 따라 통제되었습니다.
FAIL → PASS로 바뀐 WPT subtest 세 개는 두 곳의 검사 지점에 정확히 대응합니다. 한편 이미 통과하고 있던 항목도 실패하던 항목만큼이나 많은 것을 알려줍니다:
-
"A same-origin worklet importing a remote-origin script" — worklet thread에서
WorkerModuleScriptLoader::load()를 거쳐 처리되는 중첩 module import입니다. 이전에는worker-src로 검사되었습니다. -
"A remote-origin-redirected worklet" — Network process의
NetworkLoadChecker가 redirect 시점에 잡아내는 경우입니다. 이전에는 worker destination과 한 그룹으로 묶여 있었습니다. -
"A same-origin worklet importing a remote-origin-redirected script" — 두 경로를 모두 거칩니다.
"A remote-origin worklet should be blocked"는 처음부터 통과하고 있었습니다. 최상위 addModule() fetch가 별도로 검사되는 경로에 있었고, 그 경로는 이미 올바르게 동작하고 있었다는 뜻입니다. 따라서 실제로 도달 가능한 범위는 중첩 import와 redirect 시점의 재검증으로 좁혀집니다. 처음의 올바른 판단이, 그 사이 어긋나 버린 코드에 의해 다시 내려지는 두 지점입니다.
종합하면, 이 vulnerability로 인해 CSP는 origin 내부의 script 실행 경계로서 제 기능을 하지 못하게 되었습니다. 다만 memory corruption primitive를 만들어내지도 않고, sandbox를 넘지도 않습니다. worklet scope와 WorkerModuleScriptLoader는 WebContent process 안에 있습니다. NetworkLoadChecker는 Network process에서 동작하지만, 여기서 벌어진 일은 WebContent process가 시작한 load에 대해 policy 판단을 느슨하게 내린 것뿐입니다. 문제는 이미 제한된 script 실행 발판을 확보한 attacker에게 무엇이 주어지는가입니다. CSP가 억제하려는 상황이 바로 그 지점입니다. 이 결함은 아무것도 강제하지 않는 realm에서의 동적 코드 평가를 허용하고, 여기에 더해 페이지의 script-src가 금지한 origin에서 module script를 가져올 수 있게 합니다. 이번 수정은 CSP3 불변식의 양쪽을 모두 복원합니다. 이제 생성 주체인 document의 policy가 worklet 생성 시점에 전달되며, worklet fetch는 자신을 관장하는 directive를 기준으로 판정됩니다.
worklet global scope가 default 생성된 ContentSecurityPolicy를 가진 채 만들어졌고, 그 결과 eval() 통제를 포함한 worklet 내부의 모든 policy 질의가 허용으로 응답했습니다.
Insight
수정의 형태 자체가 이 문제의 성격을 드러냅니다. applyContentSecurityPolicyResponseHeaders()는 애초에 없던 함수가 아닙니다. 존재했고, 정상적으로 동작했으며, WorkerGlobalScope는 이를 올바르게 호출했습니다. 다만 계층 구조에서 잘못된 위치에 선언되어 있었습니다. 공통 base의 한쪽 subtree에 protected로 붙어 있었던 것입니다. 그래서 동일한 동작이 필요했던 형제 subtree는 아무런 신호 없이 그 동작을 전혀 갖지 못했고, 메서드 하나를 WorkerOrWorkletGlobalScope로 끌어올린 것이 구조적 수정의 전부입니다.
나머지 절반은 한 단계 아래에서 같은 주제를 반복합니다. FetchOptions.h의 isScriptLikeDestination()은 이미 두 worklet destination을 script 계열로 분류하고 있었습니다. 올바른 분류가 처음부터 같은 헤더 안에 있었지만, 정작 그것이 필요했던 두 검사 지점은 사용하지 않았습니다. isWorkletDestination()으로의 리팩터링이 단순 정리 이상의 의미를 갖는 이유입니다. 이제 각 지점은 구성원을 다시 나열하다 목록을 틀리는 대신, 범주를 이름으로 지목할 수 있습니다. 보안과 관련된 추상화, 즉 "script를 실행하는 context"를 정의하는 공통 base가 아니라 subclass 쪽에 보안 기능이 붙어 있는 경우를 생각해 볼 수 있습니다. 이후 추가되는 형제 클래스는 모두 그 기능 없이 출발합니다. 게다가 이런 누락에서는 아무런 오류도 발생하지 않습니다. 조용히 허용적인 기본값으로 귀결될 뿐입니다.
Audit directions
-
자식 context가 물려받아야 할 보안 상태를, 생성자가 복사하지 않고 기본값으로 초기화해 버리는 패턴. Narrow: WebCore의
*GlobalScope생성 경로를 하나씩 살펴봅니다.AudioWorkletGlobalScope::tryCreate,PaintWorkletGlobalScope::tryCreate,ServiceWorkerGlobalScope,SharedWorkerGlobalScope가 대상입니다. 먼저WorkletParameters와WorkerParameters의 필드를 맞대어 봅니다. 이때 보안과 관련된 항목이 전달되는지, 그리고 실제로 적용되는지를 함께 확인해야 합니다. COEP/COOP, sandbox flag, referrer policy, Trusted Types 강제, mixed-content 및 HTTPS-upgrade 상태, reporting endpoint가 여기에 해당합니다. 판별 신호는 한쪽 parameters struct에만 있고 짝이 되는 쪽에는 없는 멤버입니다. Wider: policy container를 직접 만드는 지점을 모두 검색합니다.makeUnique<ContentSecurityPolicy>(,setContentSecurityPolicy(,setSecurityOriginPolicy(형태가 대상입니다. 그다음 모든 생성 경로에서didReceiveHeaders()처럼 상속하거나 재적용하는 호출이 뒤따르는지 확인합니다. 눈여겨볼 형태는 새 policy 객체를 만들어 두고도, 같은 함수 안 어디에서도 무조건적인 상속 호출을 하지 않는 생성자입니다. Widest: 여기서 지켜야 할 invariant는 생성된 context는 자신을 만든 쪽의 policy를 상속해야 하며, 결코 기본값으로 초기화되어서는 안 된다는 것입니다. 같은 기준이 Chromium에서 worker와 worklet으로PolicyContainer를 전파하는 경로에도, Node의vmcontext 생성에도 적용됩니다. 자식 realm이 빈 권한 집합으로 만들어지는 capability 모델이나 sandbox 모델이라면 어디든 마찬가지입니다. 그래서 어디서나 던져야 할 질문은 하나입니다. 갓 생성된 policy 객체에 질의하면 무엇을 반환하며, 그 답이 허용 쪽인지 여부입니다. -
확장 가능한 enum을 기준으로 policy를 결정하면서, 새로 추가된 멤버가 허용 쪽 default나 인접한 fallthrough 그룹으로 흘러드는 패턴. Narrow:
FetchOptions::Destination을 대상으로 하는switch문과, CSP directive를 고르는destination ==비교를 전부 나열합니다.NetworkLoadChecker::isAllowedByContentSecurityPolicy,WorkerModuleScriptLoader::load,ContentSecurityPolicy.cpp,CachedResourceLoader.cpp가 출발점입니다. 그다음FetchOptions.h의EnumTraitsForPersistence목록에 있는 destination이 CSP3가 지정한 directive로 흘러가는지 확인합니다.Environmentmap,Model,Speculationrules도 빠뜨리면 안 됩니다. 신호는 두 가지인데, 하나는 본문 없는case가 다른 directive 계열의 그룹으로 흘러드는 형태입니다. 다른 하나는if (destination == X) … else …에서else쪽이 보수적인 policy 대신 특정 policy를 담고 있는 경우입니다. Wider: 같은 형태는 catch-all 분기를 둔 채 type tag로 갈라지는 allowlist라면 어디서나 나타납니다. MIME-sniffing 테이블, Permissions-Policy feature map,isNonSubresourceRequest계열의 predicate 묶음이 그런 예입니다. 그래서 하나의 enum을 두고 분류 helper가 둘 있는데 소속 집합이 서로 어긋나는 경우를 찾아야 합니다. 이번 건에서도isScriptLikeDestination()은 worklet destination을 이미 포함하고 있었지만, 정작 enforcement 지점은 그렇지 않았습니다. Widest: 누락에 의한 분류 역시 하나의 policy 결정입니다. Chromium에서network::mojom::RequestDestination을 directive로 매핑하는 부분에도, implicit-allow를 default로 둔 routing table이나 firewall rule set에도 그대로 통합니다. exhaustive match를 강제하지 않는 언어라면 어디든 마찬가지입니다. enum에 멤버를 추가해도 컴파일이 깨지지 않는다면, default 분기 자체가 곧 policy입니다. 그렇다면 그 분기는 제한적인 쪽이어야 합니다. -
멤버를 위치 순서대로 나열하는 수작업 deep-copy 및 serialization 함수. Narrow: 먼저
WorkletParameters::isolatedCopy()와WorkerParameters에서 멤버 개수와 인자 개수가 맞는지 점검합니다. 이어서Source/WebCore/workers/와Source/WebCore/worklets/아래의 다른 cross-thread parameter struct로 범위를 넓힙니다. 신호는 struct의 멤버 목록보다 짧은 brace-initializer 인자 목록입니다. 이런 코드는 아무 경고 없이 컴파일되면서 필드 하나를 기본 생성 상태로 남깁니다. 그 필드가 policy나 credential이라면, 비어 있거나 허용 쪽 값이 된다는 뜻입니다. Wider: 같은 형태는 WebKit의 모든 수작업isolatedCopy, 모든crossThreadCopy특수화, 모든 IPC encode/decode 쌍에 걸쳐 있습니다. 손으로 관리되는 복사 함수가 둘 이상 건드리는 멤버를 가진 struct를 찾고, 그것들이 함께 갱신되었는지 확인해야 합니다. 같은 파일 안의 각 clone 함수나 encode 함수에 대응하는 줄 없이 필드만 추가된 diff가 바로 그 신호입니다. Widest: 멤버를 하나씩 옮기는 수작업 복사는 struct 정의를 강제성 없이 중복해 둔 것이며, 시간이 지나면 어긋납니다. MojoStructTraits, 손으로 작성한 serde 구현, protobuf 변환 helper, ORM mapper 모두 사정이 같습니다. 그래서 던져야 할 질문은 이 복사를 구조적으로 만들 수 있는가입니다. reflection이나 코드 생성, designated initializer를 쓰면 멤버 누락이 컴파일 에러로 드러나게 됩니다. -
process 경계를 사이에 두고 중복된 enforcement 검사에서, 두 사본이 어긋나며 약한 쪽이 실질적인 기준이 되는 패턴. Narrow: CSP와 관련된 destination마다 양쪽 판단을 맞대어 봅니다. 한쪽은
WorkerModuleScriptLoader::load의 WebContent 측 판단과 거기서 전달하는ContentSecurityPolicyEnforcement값입니다. 다른 한쪽은NetworkLoadChecker::isAllowedByContentSecurityPolicy의 Network 측 판단입니다. 이번 commit이 양쪽을 모두 수정해야 했던 이유도, 같은 destination에서 두 판단이 어긋났기 때문입니다. 신호는 한 process에서 선택된 directive가 다른 process에는 동일한 키로 대응되지 않는 경우입니다. Wider: enforcement가 둘로 갈라지는 형태는, renderer에서 사전 검사를 한 번 하고 network layer에서 redirect나 response 시점에 다시 검사하는 구간마다 반복됩니다. mixed-content 차단, CORS preflight 캐싱, upgrade-insecure-requests를 두고 최초 요청만 검증되는 사례가 없는지 점검할 필요가 있습니다. 또한redirectResponseReceived형태의 flag를 받는 함수를 모두 살펴, redirect 분기가 최초 요청만큼은 강하게 검사하는지 확인해야 합니다. Widest: 하나의 policy를 두 지점에서 강제하면, 실제로 적용되는 것은 둘 중 약한 쪽입니다. client/server 검증 쌍, API gateway와 service 단 authorization의 조합, eBPF/LSM hook 중복 모두 같은 원리를 따릅니다. 그리고 어디서나 들고 다녀야 할 질문은 이것입니다. 두 구현이 어긋날 때 attacker가 고를 수 있는 쪽은 어디인지 물어야 합니다.