fix(core,extensions): 동적 스크립트 주입 경로 전부에 출처 게이트 적용
레이아웃 scripts[] 에만 있던 원격 스크립트 차단 게이트가 다른 주입 경로에는 없어, 저장측(SafeLayoutExpressions·NoExternalUrls)이 외부 URL 저장을 422 로 막아도 런타임 디스패치 한 번으로 임의 원격 코드가 로드됐다. 저장 검증이 통째로 무의미해지는 상태였고 오류도 경고도 남지 않았다. 판정식을 support/scriptSrcPolicy.ts 로 분리해 런타임 SSoT 를 하나로 두고, loadScript 액션 · 확장 핸들러 재로드 · 편집기 프리뷰 · 편집기 자산 매니페스트 · G7Core.asset.loadScript · PG 플러그인 SDK 주입이 모두 그것을 경유하게 했다. 사본을 두지 않은 이유는 차집합이 그대로 우회로가 되기 때문이다. - 게이트는 캐시 검사보다 앞에 둔다 (뒤에 두면 이미 로드된 미신뢰 스크립트가 캐시 히트로 통과한다) - NoExternalUrls 순회를 9키로 확대 — 번들 레이아웃 601개 사전 스캔으로 정당한 외부 URL 0건을 확인한 뒤 넓혔다 (저장 회귀 없음) - callExternal 은 생성자를 참조 동일성으로 거부하고(별칭 전역 포함), 프로토타입 경로 세그먼트를 읽기·매핑 키 양쪽에서 차단한다 - PG SDK 는 서비스 SDK 라 자체 호스팅이 불가능하므로 manifest 선언 + 주입 직전 호스트 확인이 게이트다. tosspayments SDK URL 은 확장자가 없어 정적 검사에 걸리지 않아 그 런타임 확인이 유일한 게이트다 - 함께 드러난 결함: 동시 loadScript 가 로드 전에 완료 처리되던 문제, 확장 재로드에서 script 기존재가 CSS 까지 건너뛰던 문제, loadCSS 의 in-flight 미공유
This commit is contained in:
1 parent
621cd28376
commit
1e89335258
82 files changed
+2706
-240
No files matched your search
@@ -397,6 +397,15 @@ export class ModuleAssetLoader {
|
||||
/**
|
||||
* CSS 파일을 동적으로 로드합니다.
|
||||
*
|
||||
* `loadJS`·`loadBundleJs` 와 같이 in-flight Promise 를 공유한다 — CSS 경로만 이
|
||||
* 계층이 없어서, 같은 확장의 CSS 를 동시에 요청하면 `<link>` 가 중복 생성되고
|
||||
* 재시도 로더가 기존 element 를 제거하면서 서로의 시도를 지웠다.
|
||||
*
|
||||
* 키는 `module-css-{id}` 로 둔다 — `loadJS` 가 raw identifier 를 키로 쓰므로,
|
||||
* 같은 키공간을 쓰면 JS 로드가 CSS 로드로 오인되어 조용히 건너뛰어진다.
|
||||
*
|
||||
* 실패해도 throw 하지 않는 기존 계약을 유지한다(`surfaceCssFailure`).
|
||||
*
|
||||
* @param identifier 모듈 식별자
|
||||
* @param url CSS 파일 URL
|
||||
*/
|
||||
@@ -409,18 +418,31 @@ export class ModuleAssetLoader {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await loadStylesheetWithRetry(url, { id: elementId }, { label: `CSS: ${identifier}` });
|
||||
|
||||
logger.log(`CSS loaded: ${identifier}`);
|
||||
|
||||
const link = document.getElementById(elementId);
|
||||
if (link) {
|
||||
this.registerLoadedAsset(identifier, { type: 'css', element: link });
|
||||
}
|
||||
} catch (error) {
|
||||
this.surfaceCssFailure(identifier, identifier, url, error);
|
||||
// 이미 로딩 중인 경우 대기 (JS 키공간과 겹치지 않는 elementId 를 키로 쓴다)
|
||||
const existingPromise = this.loadingPromises.get(elementId);
|
||||
if (existingPromise) {
|
||||
logger.log(`CSS already loading: ${identifier}`);
|
||||
return existingPromise;
|
||||
}
|
||||
|
||||
const loadPromise = loadStylesheetWithRetry(url, { id: elementId }, { label: `CSS: ${identifier}` })
|
||||
.then(() => {
|
||||
logger.log(`CSS loaded: ${identifier}`);
|
||||
|
||||
const link = document.getElementById(elementId);
|
||||
if (link) {
|
||||
this.registerLoadedAsset(identifier, { type: 'css', element: link });
|
||||
}
|
||||
this.loadingPromises.delete(elementId);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.loadingPromises.delete(elementId);
|
||||
this.surfaceCssFailure(identifier, identifier, url, error);
|
||||
});
|
||||
|
||||
this.loadingPromises.set(elementId, loadPromise);
|
||||
|
||||
return loadPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user