허용목록이 플러그인 코드 상수라, 새 확장이 저장 키를 도입할 때마다 이 플러그인을 고쳐야 했다. 고치지 않으면 그 키는 방문마다 파기되는데 예외도 로그도 남지 않아 운영자에게는 "설정이 저장되지 않는다" 는 증상만 보인다. 허용목록을 운영자 설정(necessary_storage_allowlist)으로 옮기고, 관리자 환경설정에 브라우저 저장소·세션 저장소·쿠키 세 카드를 두어 직접 편집하게 했다. 확장은 GDPR 을 알 필요가 없고 GDPR 도 확장을 알 필요가 없다 — 연결은 운영자가 화면에서 한다. 저장 키를 플러그인이 관측·수집하지 않는 것도 같은 이유다: 관측 자체가 추적이다. 판정은 서버(미들웨어)와 클라이언트 셋(인터셉터 둘 + 정리기)이 같은 설정을 같은 매칭 규칙으로 읽는다. 잠금 항목(로그인 토큰·CSRF·세션 쿠키·동의 기록)만 설정 밖에 두고 판정 시점에 합집합으로 얹는다 — 설정에 담기면 저장 요청 한 번으로 지워져 잠금이 아니게 된다. 기설치본은 업그레이드 스텝이 현재 카탈로그를 설정 파일에 못박되, 운영자가 이미 편집했으면 그대로 둔다. 함께 고친 선재 결함 셋: - 차단 도메인 추천 목록이 응답에 실리지 않아 자동완성이 동작하지 않던 문제 - 세션 쿠키 이름을 기본값에서 바꾼 사이트에서 브라우저 측 목록의 그 항목이 죽던 문제 - 쿠키 목록에만 앞부분 매칭(`이름_*`)이 적용되지 않던 문제
151 lines
4.8 KiB
TypeScript
151 lines
4.8 KiB
TypeScript
/**
|
|
* GDPR 1st-party Storage 가로채기 (storageInterceptor)
|
|
*
|
|
* `Storage.prototype.setItem` 을 가로채 functional 카테고리 미동의 상태에서
|
|
* 신규 localStorage / sessionStorage 쓰기를 차단. EDPB Guidelines 2/2023 §16
|
|
* "동의 전 사전 차단 (prior consent before storage)" 충족.
|
|
*
|
|
* 게이팅 규칙 (Phase 2 단순화 — 4단계):
|
|
* 1. strictly necessary allowlist 매칭 → 항상 허용
|
|
* 2. functional 동의 → 허용
|
|
* 3. user-initiated 면제 (WP29 §3.6, 항상 활성) → 사용자 인터랙션 직후 허용
|
|
* 4. 그 외 → 차단
|
|
*
|
|
* 허용목록은 **운영자 설정**(`necessary_storage_allowlist`)이며 이 파일에 사본을 두지
|
|
* 않는다. 사본을 두면 운영자가 화면에서 추가한 항목과 코드 상수가 갈라져, 화면에는 등재된
|
|
* 키가 실제로는 파기되는 상태가 된다. 판정 함수도 세 소비자가 공유한다
|
|
* (`necessaryAllowlist.ts`).
|
|
*
|
|
* "운영자 등록 표" 는 제거됨 — GDPR 원칙은 "strictly necessary 외 비-필수 저장은
|
|
* 동의 전 차단" 이므로 등록 표 없이 동일하게 모두 게이팅.
|
|
*
|
|
* window.localStorage.setItem 과 window.sessionStorage.setItem 은 둘 다
|
|
* Storage.prototype.setItem 을 공유하므로 1회 가로채기로 양쪽 모두 커버.
|
|
*
|
|
* @module sirsoft-gdpr/storageInterceptor
|
|
*/
|
|
|
|
import {
|
|
LOCKED_FALLBACK,
|
|
isNecessary,
|
|
type NecessaryAllowlist,
|
|
} from './necessaryAllowlist';
|
|
import { isUserInitiated } from './userInitiatedTracker';
|
|
|
|
/**
|
|
* Storage 종류 — strictly necessary 판정 시 storage 타입 매칭에 사용.
|
|
*/
|
|
export type StorageKind = 'localStorage' | 'sessionStorage';
|
|
|
|
/**
|
|
* 인터셉터 설정.
|
|
*
|
|
* @property functionalConsented functional 카테고리 동의 여부
|
|
* @property necessaryAllowlist strictly necessary 허용목록 (운영자 설정 ∪ 잠금 집합)
|
|
*/
|
|
export interface StorageInterceptorConfig {
|
|
functionalConsented: boolean;
|
|
necessaryAllowlist: NecessaryAllowlist;
|
|
}
|
|
|
|
let installed = false;
|
|
let originalSetItem: ((key: string, value: string) => void) | null = null;
|
|
let config: StorageInterceptorConfig = {
|
|
functionalConsented: false,
|
|
necessaryAllowlist: LOCKED_FALLBACK,
|
|
};
|
|
|
|
/**
|
|
* setItem 호출이 허용되는지 판정합니다.
|
|
*
|
|
* 본 함수는 사이드 이펙트 없음 — 정책 평가만. 차단/통과 결정은 호출자가 수행.
|
|
*
|
|
* 게이팅 규칙 (4단계):
|
|
* 1. strictly necessary → 허용
|
|
* 2. functional 동의 → 허용
|
|
* 3. user-initiated (WP29 §3.6, 항상 활성) → 허용
|
|
* 4. 그 외 → 차단
|
|
*
|
|
* @param key 스토리지 키
|
|
* @param storage 호출 스토리지 종류
|
|
* @return 허용 여부
|
|
*/
|
|
export function isStorageAllowed(key: string, storage: StorageKind): boolean {
|
|
if (isNecessary(key, storage, config.necessaryAllowlist)) {
|
|
return true;
|
|
}
|
|
|
|
if (config.functionalConsented) {
|
|
return true;
|
|
}
|
|
|
|
// user-initiated 면제 (WP29 §3.6) — 사용자가 직접 트리거한 일회성 설정은 동의 없이 허용.
|
|
// GDPR 표준 면제로 항상 활성 (운영자 토글 없음).
|
|
if (isUserInitiated()) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 인터셉터를 설치합니다 — Storage.prototype.setItem 을 가로채기.
|
|
*
|
|
* 중복 install 방지: installed 플래그.
|
|
*
|
|
* @param initialConfig 초기 설정 (이후 updateStorageInterceptorConfig 로 갱신)
|
|
* @return void
|
|
*/
|
|
export function installStorageInterceptor(initialConfig: StorageInterceptorConfig): void {
|
|
if (installed) {
|
|
return;
|
|
}
|
|
installed = true;
|
|
config = initialConfig;
|
|
|
|
const proto = Storage.prototype;
|
|
originalSetItem = proto.setItem;
|
|
|
|
proto.setItem = function (this: Storage, key: string, value: string): void {
|
|
const storage: StorageKind = this === window.sessionStorage ? 'sessionStorage' : 'localStorage';
|
|
if (!isStorageAllowed(key, storage)) {
|
|
return;
|
|
}
|
|
if (originalSetItem) {
|
|
originalSetItem.call(this, key, value);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 인터셉터 설정을 갱신합니다 — 동의 변경 시 호출.
|
|
*
|
|
* @param newConfig 갱신할 설정
|
|
* @return void
|
|
*/
|
|
export function updateStorageInterceptorConfig(newConfig: StorageInterceptorConfig): void {
|
|
config = newConfig;
|
|
}
|
|
|
|
/**
|
|
* 인터셉터를 해제합니다 (테스트 격리 / cleanup 용).
|
|
*
|
|
* 원본 setItem 을 복원하고 installed 플래그를 리셋.
|
|
*
|
|
* @return void
|
|
*/
|
|
export function uninstallStorageInterceptor(): void {
|
|
if (!installed) {
|
|
return;
|
|
}
|
|
if (originalSetItem) {
|
|
Storage.prototype.setItem = originalSetItem;
|
|
}
|
|
originalSetItem = null;
|
|
config = {
|
|
functionalConsented: false,
|
|
necessaryAllowlist: LOCKED_FALLBACK,
|
|
};
|
|
installed = false;
|
|
}
|