Files
Gnuboard7/plugins/_bundled/sirsoft-gdpr/resources/js/storageInterceptor.ts
T
HeuJung 1124fad027 feat(gdpr): 필수 저장 항목 허용목록을 운영자 설정으로 이관
허용목록이 플러그인 코드 상수라, 새 확장이 저장 키를 도입할 때마다 이 플러그인을
고쳐야 했다. 고치지 않으면 그 키는 방문마다 파기되는데 예외도 로그도 남지 않아
운영자에게는 "설정이 저장되지 않는다" 는 증상만 보인다.

허용목록을 운영자 설정(necessary_storage_allowlist)으로 옮기고, 관리자 환경설정에
브라우저 저장소·세션 저장소·쿠키 세 카드를 두어 직접 편집하게 했다. 확장은 GDPR 을
알 필요가 없고 GDPR 도 확장을 알 필요가 없다 — 연결은 운영자가 화면에서 한다.
저장 키를 플러그인이 관측·수집하지 않는 것도 같은 이유다: 관측 자체가 추적이다.

판정은 서버(미들웨어)와 클라이언트 셋(인터셉터 둘 + 정리기)이 같은 설정을 같은 매칭
규칙으로 읽는다. 잠금 항목(로그인 토큰·CSRF·세션 쿠키·동의 기록)만 설정 밖에 두고
판정 시점에 합집합으로 얹는다 — 설정에 담기면 저장 요청 한 번으로 지워져 잠금이
아니게 된다. 기설치본은 업그레이드 스텝이 현재 카탈로그를 설정 파일에 못박되,
운영자가 이미 편집했으면 그대로 둔다.

함께 고친 선재 결함 셋:
- 차단 도메인 추천 목록이 응답에 실리지 않아 자동완성이 동작하지 않던 문제
- 세션 쿠키 이름을 기본값에서 바꾼 사이트에서 브라우저 측 목록의 그 항목이 죽던 문제
- 쿠키 목록에만 앞부분 매칭(`이름_*`)이 적용되지 않던 문제
2026-09-03 22:18:18 +09:00

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;
}