haechi 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +10 -1
- package/README.md +10 -1
- package/docs/current/api-stability.ko.md +9 -5
- package/docs/current/api-stability.md +9 -5
- package/docs/current/configuration.ko.md +106 -2
- package/docs/current/configuration.md +106 -2
- package/docs/current/release-0.9-implementation-scope.ko.md +231 -0
- package/docs/current/release-0.9-implementation-scope.md +231 -0
- package/docs/current/release-process.ko.md +7 -1
- package/docs/current/release-process.md +7 -1
- package/docs/current/risk-register-release-gate.ko.md +17 -6
- package/docs/current/risk-register-release-gate.md +15 -4
- package/docs/current/threat-model.ko.md +16 -1
- package/docs/current/threat-model.md +16 -1
- package/haechi.config.example.json +2 -1
- package/package.json +1 -1
- package/packages/audit/index.mjs +12 -1
- package/packages/cli/runtime.mjs +5 -1
- package/packages/core/index.mjs +4 -0
- package/packages/filter/index.mjs +58 -3
- package/packages/proxy/index.mjs +3 -0
|
@@ -69,6 +69,8 @@ npm audit signatures
|
|
|
69
69
|
| `.github/workflows/npm-publish.yml` | `haechi` | `v<semver>` | npm provenance publish + checksummed/attested release assets |
|
|
70
70
|
| `.github/workflows/crypto-kms-publish.yml` | `haechi-crypto-kms` | `crypto-kms-v<semver>` | satellite publish, same signed-artifacts path |
|
|
71
71
|
| `.github/workflows/auth-jwt-publish.yml` | `haechi-auth-jwt` | `auth-jwt-v<semver>` | satellite publish, same signed-artifacts path |
|
|
72
|
+
| `.github/workflows/dashboard-publish.yml` | `haechi-dashboard` | `dashboard-v<semver>` | satellite publish, same signed-artifacts path |
|
|
73
|
+
| `.github/workflows/auth-oidc-publish.yml` | `haechi-auth-oidc` | `auth-oidc-v<semver>` | satellite publish, same signed-artifacts path |
|
|
72
74
|
|
|
73
75
|
Each publish workflow triggers on `release: published` but is **guarded** so the two never cross-fire: the core job runs only for tags starting `v` (and re-validates `^v[0-9]+\.[0-9]+\.[0-9]+$`); the satellite job runs only for `crypto-kms-v…` (and re-validates `^crypto-kms-v[0-9]+\.[0-9]+\.[0-9]+$` **and** that the tag version equals the satellite's `package.json` version). The npmjs.com Trusted Publisher for each package is bound to its **specific workflow filename** — renaming a workflow file breaks its OIDC publish until the npm config is updated.
|
|
74
76
|
|
|
@@ -89,6 +91,8 @@ No manual `npm publish` from a laptop is needed. Because the names are unscoped
|
|
|
89
91
|
|---|---|---|---|
|
|
90
92
|
| `haechi-crypto-kms` | `crypto-kms-v<semver>` | `crypto-kms-publish.yml` | `satellites/crypto-kms/package.json` |
|
|
91
93
|
| `haechi-auth-jwt` | `auth-jwt-v<semver>` | `auth-jwt-publish.yml` | `satellites/auth-jwt/package.json` |
|
|
94
|
+
| `haechi-dashboard` | `dashboard-v<semver>` | `dashboard-publish.yml` | `satellites/dashboard/package.json` |
|
|
95
|
+
| `haechi-auth-oidc` | `auth-oidc-v<semver>` | `auth-oidc-publish.yml` | `satellites/auth-oidc/package.json` |
|
|
92
96
|
|
|
93
97
|
**Verify a satellite release** (same anchors as core):
|
|
94
98
|
|
|
@@ -97,7 +101,9 @@ gh attestation verify haechi-crypto-kms-<version>.tgz --repo raeseoklee/haechi
|
|
|
97
101
|
npm view haechi-crypto-kms --json # dist.attestations present; access "public"
|
|
98
102
|
```
|
|
99
103
|
|
|
100
|
-
**Dependency note:** `haechi-crypto-kms` keeps core zero-dependency — `@aws-sdk/client-kms` is an **optional peer dependency**, imported lazily only when a real AWS client is used and not injected. Consumers who use the in-memory or an injected client never install the SDK.
|
|
104
|
+
**Dependency note:** `haechi-crypto-kms` keeps core zero-dependency — `@aws-sdk/client-kms` is an **optional peer dependency**, imported lazily only when a real AWS client is used and not injected. Consumers who use the in-memory or an injected client never install the SDK. The 0.2.0 `./gcp` (`@google-cloud/kms`) and `./azure` (`@azure/keyvault-keys` + `@azure/identity`) backends follow the same optional-peer/lazy-import model; the `./vault` backend has zero optional peer (`node:` `fetch` only).
|
|
105
|
+
|
|
106
|
+
**0.9 satellites (new unscoped names — configure Trusted Publisher *before* the first tag):** `haechi-dashboard` and `haechi-auth-oidc` are first-published in 0.9 and follow the same per-satellite bootstrap order above. As with the 0.8 satellites, the unscoped name is claimed on first OIDC publish, so the npmjs.com Trusted Publisher for each must be configured **before** its first tag — link `raeseoklee/haechi` and the exact workflow filename (`dashboard-publish.yml` for `haechi-dashboard`, `auth-oidc-publish.yml` for `haechi-auth-oidc`), then push the prefixed tag (`dashboard-v0.1.0`, `auth-oidc-v0.1.0`) and publish the GitHub Release. The two existing satellites ride their already-bootstrapped tags/workflows: `haechi-auth-jwt@0.2.0` on `auth-jwt-v<semver>` (`auth-jwt-publish.yml`) and `haechi-crypto-kms@0.2.0` on `crypto-kms-v<semver>` (`crypto-kms-publish.yml`) — no new Trusted Publisher configuration is required for those two.
|
|
101
107
|
|
|
102
108
|
## 6. Deployment block conditions
|
|
103
109
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Haechi 리스크 레지스터 및 릴리스 게이트
|
|
2
2
|
|
|
3
|
-
- 문서 상태: Draft 0.
|
|
4
|
-
- 작성일: 2026-06-
|
|
5
|
-
- 기준 버전: 0.
|
|
3
|
+
- 문서 상태: Draft 0.4
|
|
4
|
+
- 작성일: 2026-06-11
|
|
5
|
+
- 기준 버전: 0.9.0
|
|
6
6
|
- 기준 브랜치: `main`
|
|
7
7
|
|
|
8
8
|
## 1. 현재 판단
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
| G1 | GitHub pre-release | P0 코드 리스크 해결, production-ready 표현 없음 | Pass |
|
|
26
26
|
| G2 | npm developer preview | P0 해결, preflight/SBOM/provenance 경로 준비, npm auth 확인 | Pass (`haechi@0.3.2` 2026-06-10 배포) |
|
|
27
27
|
| G3 | npm stable | P1 운영 reference, stream-aware enforcement, API stability 강화 | Blocked |
|
|
28
|
+
| G4 | 0.9.0 observability + interactive-auth 위성 컷 | P1-SEC-009 (0.9) / P1-OPS-005 (0.9) mitigated 및 P2-CRYPTO-001 (0.9) accepted; `haechi-dashboard` + `haechi-auth-oidc` + `haechi-crypto-kms@0.2.0` 테스트 통과; 위성 tarball zero-dep; core 0.9.0 bump(추가적 FORBIDDEN_KEYS audit 강화만) | Pass |
|
|
28
29
|
|
|
29
30
|
## 3. P0 배포 차단 리스크 상태
|
|
30
31
|
|
|
@@ -96,6 +97,16 @@
|
|
|
96
97
|
|
|
97
98
|
base64/인코딩 값 디코딩 검사, query string 검사, audit tail truncation 탐지는 명시적 제외로 threat model에 문서화했다 (0.4+ backlog).
|
|
98
99
|
|
|
100
|
+
## 5.3 0.9.0 Observability + Interactive-Auth 리스크 상태
|
|
101
|
+
|
|
102
|
+
이 ID들은 0.9.0 위성 컷(`haechi-dashboard`, `haechi-auth-oidc`, `haechi-crypto-kms@0.2.0`)에 한정되며, 0.9.0 섹션으로 namespace되어 위의 동일 번호 P0/P1 행과 구분된다. 증거는 위성 소스, 그 테스트 스위트, 그리고 `docs/current/release-0.9-implementation-scope.md` §6에 정리된 adversarial security review다.
|
|
103
|
+
|
|
104
|
+
| ID | 리스크 | 상태 | 해소 증거 |
|
|
105
|
+
|---|---|---|---|
|
|
106
|
+
| P1-SEC-009 (0.9) | OIDC broker 세션/로그인 보안: `haechi-auth-oidc`의 login CSRF, authorization-code injection, open-redirect, session fixation, mix-up(잘못된 IdP/RP) | Mitigated | `satellites/auth-oidc/index.mjs`: state-first `/auth/callback`(pre-auth 쿠키 바인딩 pending record를 atomic `take()` + egress 이전 constant-time `state` 비교), PKCE S256, callback에서 새 세션 id 발급(fixation 없음), `returnToAllowlist`(open-redirect 없음), issuer/endpoint pinning + RFC 9207 `iss` 검사 + 공유 `createJwtVerifier` 경유 ID-token `aud`/`azp` 프로파일(mix-up), CSRF-gated non-GET logout. `satellites/auth-oidc/auth-oidc.test.mjs`가 각 deny 케이스 검증; scope §6 adversarial review. **잔여:** multi-origin IdP는 범위 외 |
|
|
107
|
+
| P1-OPS-005 (0.9) | Dashboard audit 노출: `haechi-dashboard`의 `detections[].path` stored XSS, 미래 필드 audit leak, localhost 뷰어 DNS-rebinding 읽기, remote bind 시 인증 없는 읽기 | Mitigated | `satellites/dashboard/index.mjs` + `assets.mjs`: 엄격 CSP(`require-trusted-types-for 'script'`) + `textContent`-only 렌더링(XSS), `FORBIDDEN_KEYS` 위 재귀적 key-by-key allowlist projection(필드 leak), 요청별 anti-rebinding `Host` allowlist + CORP/COOP same-origin(rebinding), `sessionGuard` **및** TLS 종단을 요구하는 fail-closed remote bind(인증 없는 remote 읽기). `satellites/dashboard/dashboard.test.mjs`; scope §6 adversarial review. **잔여:** remote bind 시 운영자가 TLS 종단을 책임 |
|
|
108
|
+
| P2-CRYPTO-001 (0.9) | KMS backend egress: `haechi-crypto-kms@0.2.0` Vault/GCP/Azure backend가 key material이나 provider/key-path 상세를 유출하거나 의도치 않은(metadata) 엔드포인트에 도달 가능 | Accepted | `satellites/crypto-kms/{vault,gcp,azure}.mjs`: optional-peer + injected-client 모델과 faithful-mock `assertCryptoProviderConformance`(cross-key·corrupted-blob 거부, HMAC determinism/domain-separation), Vault `fetch`의 satellite-local `isBlockedAddress` SSRF 가드(dev-only `satellites/crypto-kms/ssrf-parity.test.mjs`로 auth-jwt와 parity 유지), generic fail-closed provider-error 매핑(audit에 provider/key-ARN 없음). `{vault,gcp,azure}.test.mjs` + `crypto-kms.test.mjs`; scope §6 adversarial review. **수용된 잔여:** 실제 Vault/GCP/Azure live-backend 검증은 CI 외부; 발행 tarball은 zero runtime dependency 유지 |
|
|
109
|
+
|
|
99
110
|
## 6. P2 제품/문서 리스크 상태
|
|
100
111
|
|
|
101
112
|
| ID | 기존 리스크 | 상태 | 해소 증거 |
|
|
@@ -128,9 +139,9 @@ base64/인코딩 값 디코딩 검사, query string 검사, audit tail truncatio
|
|
|
128
139
|
|---|---|---|
|
|
129
140
|
| 0.4.0 ✅ | token round-trip and adoption | 2026-06-10 구현 완료: 요청 스코프 response detokenization, deterministic tokenization(파생 키), `haechi mcp-wrap`, `haechi audit-verify`/`haechi status`, injection detection type(기본 allow), `identity`/`authProvider` 계약 예약. `docs/current/release-0.4-implementation-scope.md` 참조 |
|
|
130
141
|
| 0.5.0 ✅ | streaming hardening | 2026-06-10 출시: bounded cross-frame 버퍼를 사용한 SSE/NDJSON 스트리밍 응답 검사(`streaming.requestMode: inspect`). stream sequence AAD, replay cache, 강화된 원격 배포 가이드는 0.6+으로 이월. `docs/current/release-0.5-implementation-scope.md` 참조 |
|
|
131
|
-
| 0.6.0 ✅ | Shipped 2026-06-10 (PRs #17–#19): built-in bearer auth, named policy profiles, model allowlist, request rate limit, PII-safe identity in audit. `docs/current/release-0.6-implementation-scope.md` 참조 |
|
|
132
|
-
| 0.7.0 ✅ | Shipped 2026-06-10 (PRs #22–#24): audit head-hash anchoring + external sink contract, cryptoProvider contract hardening + `assertCryptoProviderConformance` + reference KMS adapter, 서명/체크섬된 release artifact. `docs/current/release-0.7-implementation-scope.md` 참조 |
|
|
133
|
-
| 0.8.0
|
|
142
|
+
| 0.6.0 ✅ | auth and per-client controls | Shipped 2026-06-10 (PRs #17–#19): built-in bearer auth, named policy profiles, model allowlist, request rate limit, PII-safe identity in audit. `docs/current/release-0.6-implementation-scope.md` 참조 |
|
|
143
|
+
| 0.7.0 ✅ | ops hardening | Shipped 2026-06-10 (PRs #22–#24): audit head-hash anchoring + external sink contract, cryptoProvider contract hardening + `assertCryptoProviderConformance` + reference KMS adapter, 서명/체크섬된 release artifact. `docs/current/release-0.7-implementation-scope.md` 참조 |
|
|
144
|
+
| 0.8.0 ✅ | ecosystem foundation + satellites | 2026-06-10 출시(PR #27–#32): npm workspaces 모노레포(루트 자기참조 `["."]` + `satellites/*`); `haechi@0.8.0`(attested), `haechi-crypto-kms`, `haechi-auth-jwt`(unscoped — `@haechi` scope 점유됨) **발행 완료**. core는 zero runtime dependency 유지(CI no-leak + zero-dep + satellite-packaging 게이트). 위성 `0.1.0`은 이름 생성을 위한 수동 부트스트랩 발행(unattested, `--provenance=false`, `0.3.2`와 동일한 갭)으로 per-name Trusted Publisher 설정 후, `0.1.1`이 첫 attested CI 릴리스(SLSA provenance + sigstore, `gh attestation verify` 통과). `docs/current/release-0.8-implementation-scope.md` 참조 |
|
|
134
145
|
| 0.9.0 | observability + interactive auth | `haechi-auth-oidc` 전체 authorization-code flow, `haechi-dashboard` 읽기 전용 audit 뷰어(hash-chain 무결성 표시, 요약/검색/타임라인), `haechi-crypto-kms` 추가 백엔드(Vault/GCP/Azure) |
|
|
135
146
|
| 1.0.0 | stable API contract | migration policy, long-term audit schema, plugin sandbox/runtime conformance 및 allowlist/manifest 통과 외부 auth/classifier package 동적 로딩 |
|
|
136
147
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Haechi Risk Register and Release Gates
|
|
2
2
|
|
|
3
|
-
- Status: Draft 0.
|
|
4
|
-
- Date: 2026-06-
|
|
5
|
-
- Target version: 0.
|
|
3
|
+
- Status: Draft 0.4
|
|
4
|
+
- Date: 2026-06-11
|
|
5
|
+
- Target version: 0.9.0
|
|
6
6
|
- Branch: `main`
|
|
7
7
|
|
|
8
8
|
## 1. Current Assessment
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
| G1 | GitHub pre-release | P0 code risks resolved, no production-ready language | Pass |
|
|
26
26
|
| G2 | npm developer preview | P0 resolved, preflight/SBOM/provenance paths ready, npm auth confirmed | Pass (`haechi@0.3.2` published 2026-06-10) |
|
|
27
27
|
| G3 | npm stable | P1 production reference, stream-aware enforcement, API stability hardened | Blocked |
|
|
28
|
+
| G4 | 0.9.0 observability + interactive-auth satellite cut | P1-SEC-009 (0.9) / P1-OPS-005 (0.9) mitigated and P2-CRYPTO-001 (0.9) accepted; `haechi-dashboard` + `haechi-auth-oidc` + `haechi-crypto-kms@0.2.0` tests green; satellite tarballs zero-dep; core bumped to 0.9.0 (only an additive FORBIDDEN_KEYS audit hardening) | Pass |
|
|
28
29
|
|
|
29
30
|
## 3. P0 Distribution-Blocking Risk Status
|
|
30
31
|
|
|
@@ -96,6 +97,16 @@
|
|
|
96
97
|
|
|
97
98
|
Base64/encoded-value decode inspection, query-string inspection, and audit tail truncation detection are explicitly excluded and documented in the threat model (0.4+ backlog).
|
|
98
99
|
|
|
100
|
+
## 5.3 0.9.0 Observability + Interactive-Auth Risk Status
|
|
101
|
+
|
|
102
|
+
These IDs are scoped to the 0.9.0 satellite cut (`haechi-dashboard`, `haechi-auth-oidc`, `haechi-crypto-kms@0.2.0`); they are namespaced by the 0.9.0 section and are distinct from the like-numbered P0/P1 rows above. Evidence is the satellite source, its test suite, and the adversarial security review captured in `docs/current/release-0.9-implementation-scope.md` §6.
|
|
103
|
+
|
|
104
|
+
| ID | Risk | Status | Resolution evidence |
|
|
105
|
+
|---|---|---|---|
|
|
106
|
+
| P1-SEC-009 (0.9) | OIDC broker session/login security: login CSRF, authorization-code injection, open-redirect, session fixation, and mix-up (wrong IdP/RP) in `haechi-auth-oidc` | Mitigated | `satellites/auth-oidc/index.mjs`: state-first `/auth/callback` (atomic `take()` of a pre-auth-cookie-bound pending record + constant-time `state` compare before any egress), PKCE S256, fresh session id minted at callback (no fixation), `returnToAllowlist` (no open-redirect), issuer/endpoint pinning + RFC 9207 `iss` check + ID-token `aud`/`azp` profile via the shared `createJwtVerifier` (mix-up), CSRF-gated non-GET logout. `satellites/auth-oidc/auth-oidc.test.mjs` exercises each deny case; adversarial review in scope §6. **Residual:** multi-origin IdP out of scope |
|
|
107
|
+
| P1-OPS-005 (0.9) | Dashboard audit exposure: stored XSS via `detections[].path`, future-field audit leak, DNS-rebinding read of a localhost viewer, and unauthenticated read on remote bind in `haechi-dashboard` | Mitigated | `satellites/dashboard/index.mjs` + `assets.mjs`: strict CSP (`require-trusted-types-for 'script'`) + `textContent`-only rendering (XSS), recursive key-by-key allowlist projection over `FORBIDDEN_KEYS` (field leak), per-request anti-rebinding `Host` allowlist + CORP/COOP same-origin (rebinding), fail-closed remote bind requiring `sessionGuard` **and** TLS termination (unauthenticated remote read). `satellites/dashboard/dashboard.test.mjs`; adversarial review in scope §6. **Residual:** operator must terminate TLS for remote bind |
|
|
108
|
+
| P2-CRYPTO-001 (0.9) | KMS backend egress: the `haechi-crypto-kms@0.2.0` Vault/GCP/Azure backends could leak key material or provider/key-path detail or reach an unintended (metadata) endpoint | Accepted | `satellites/crypto-kms/{vault,gcp,azure}.mjs`: optional-peer + injected-client model with faithful-mock `assertCryptoProviderConformance` (cross-key + corrupted-blob rejection, HMAC determinism/domain-separation), satellite-local `isBlockedAddress` SSRF guard on the Vault `fetch` (kept honest by the dev-only `satellites/crypto-kms/ssrf-parity.test.mjs` vs auth-jwt), generic fail-closed provider-error mapping (no provider/key-ARN in audit). `{vault,gcp,azure}.test.mjs` + `crypto-kms.test.mjs`; adversarial review in scope §6. **Residual accepted:** live-backend (real Vault/GCP/Azure) validation is out of CI; the published tarball stays zero runtime dependency |
|
|
109
|
+
|
|
99
110
|
## 6. P2 Product/Documentation Risk Status
|
|
100
111
|
|
|
101
112
|
| ID | Risk | Status | Resolution evidence |
|
|
@@ -130,7 +141,7 @@ All checklist items below were completed for 0.3.2 on 2026-06-10 except the prov
|
|
|
130
141
|
| 0.5.0 ✅ | Streaming hardening | Shipped 2026-06-10: SSE/NDJSON streaming response inspection with bounded cross-frame buffer (`streaming.requestMode: inspect`). Stream sequence AAD, replay cache, stronger remote deployment guide deferred to 0.6+. See `docs/current/release-0.5-implementation-scope.md` |
|
|
131
142
|
| 0.6.0 ✅ | Auth and per-client controls | Shipped 2026-06-10 (PRs #17–#19): built-in bearer auth, named policy profiles, model allowlist, request rate limit, PII-safe identity in audit. See `docs/current/release-0.6-implementation-scope.md` |
|
|
132
143
|
| 0.7.0 ✅ | Ops hardening | Shipped 2026-06-10 (PRs #22–#24): audit head-hash anchoring + external sink contract, cryptoProvider contract hardening + `assertCryptoProviderConformance` + reference KMS adapter, signed/checksummed release artifacts. See `docs/current/release-0.7-implementation-scope.md` |
|
|
133
|
-
| 0.8.0
|
|
144
|
+
| 0.8.0 ✅ | Ecosystem foundation + satellites | Shipped 2026-06-10 (PRs #27–#32): npm workspaces monorepo (root self-member `["."]` + `satellites/*`); **published** `haechi@0.8.0` (attested), `haechi-crypto-kms` and `haechi-auth-jwt` (unscoped — the `@haechi` scope was taken). Core stays zero runtime dependency (CI no-leak + zero-dep + satellite-packaging gates). Satellite `0.1.0` was a manual bootstrap publish (unattested, `--provenance=false`, mirroring the `0.3.2` gap) to create the names so per-name Trusted Publishers could be configured; `0.1.1` is the first attested CI release (SLSA provenance + sigstore, `gh attestation verify` passes). See `docs/current/release-0.8-implementation-scope.md` |
|
|
134
145
|
| 0.9.0 | Observability + interactive auth | `haechi-auth-oidc` full authorization-code flow, `haechi-dashboard` read-only audit viewer (hash-chain integrity display, summary/search/timeline), additional `haechi-crypto-kms` backends (Vault/GCP/Azure) |
|
|
135
146
|
| 1.0.0 | Stable API contract | Migration policy, long-term audit schema, plugin sandbox/runtime conformance, and dynamic loading of external auth/classifier packages that pass allowlist/manifest |
|
|
136
147
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
- 문서 상태: Draft 0.1
|
|
4
4
|
- 작성일: 2026-06-10
|
|
5
|
-
- 기준 버전: 0.
|
|
5
|
+
- 기준 버전: 0.9.0
|
|
6
6
|
|
|
7
7
|
## 1. 보호 대상
|
|
8
8
|
|
|
@@ -54,6 +54,17 @@ Haechi가 보호하려는 주요 자산은 다음이다.
|
|
|
54
54
|
| audit에 원시 credentials/identity 노출 | audit 로그를 통한 token 또는 subject 유출 | Token은 keyed-HMAC 해시로만 저장; identity subject/issuer는 keyed HMAC 처리; `auth_denied` 레코드에 token 미포함 |
|
|
55
55
|
| token round-trip의 타 토큰 복원 | 클라이언트/요청 간 평문 복구 | detokenization은 opt-in(`detokenizeResponses`)이며 요청 스코프: 같은 요청을 보호하며 발급된 토큰만 복원 |
|
|
56
56
|
| tool result/응답 내 간접 prompt injection | 심어진 지시문에 의한 agent 조작 | 응답 방향 휴리스틱, 기본 report-only(`injection` action `allow`), 격상은 명시적 정책 선택. 완전 방어 아님 |
|
|
57
|
+
| Haechi 자체 변환 마커 재탐지 | 모델이 echo한 토큰 왕복이 재탐지됨(예: `[TOKEN:…]`가 `secret`으로 차단) → response-enforce에서 `detokenizeResponses` 깨짐 | **응답 방향에서만** Haechi 마커(`[TOKEN:…]`, `[HAECHI_ENC:…]`, `[REDACTED:…]`) 탐지 제외. 요청 방향은 영향 없으므로 요청에 마커 모양 문자열로 secret을 숨겨 우회할 수 없음. **수용된 잔여:** 악의적 *upstream*이 누출 값을 가짜 응답 마커로 감싸 응답 방향 탐지를 회피할 수 있음 — 응답 검사는 2차 방어(모델은 semi-trusted)이고 제외는 positional(마커 구간만 건너뜀; 인접 값은 여전히 탐지됨) |
|
|
58
|
+
| 응답 메타데이터 오탐 | `enforce` 응답 검사가 envelope 메타데이터를 PII/secret으로 오인해 정상 응답을 차단(예: unix 타임스탬프 `created`가 phone 규칙에, 나노초 `*_duration`이 `card`에 매치) | KR phone 규칙이 구분자·`0` 없는 맨 숫자열을 무시; `chatcmpl-…` 같은 id는 secret 모양 아님; **응답 방향은 bare JSON number leaf를 검사하지 않음**(`*_duration`/count/timestamp/numeric-id — 모델 누출 card/RRN이 아님; 실제 누출은 생성 *텍스트*에 나타나 여전히 검사됨). **수용된 잔여:** 악의적 모델이 bare 응답 숫자로 인코딩해 유출 가능(응답 검사는 2차 방어) — 엄격 운영자는 `responseProtection.scanNumbers: true`로 재활성화 가능. 실제 vLLM·Ollama 응답은 이제 clean; 추가 주의 시 `responseProtection.mode: report-only`(탐지·감사만, 차단 없음) |
|
|
59
|
+
| Dashboard audit 뷰어 XSS — attacker-controlled `detections[].path` (0.9) | `haechi-dashboard`의 stored XSS: `<img onerror>` 같은 요청 JSON key가 client-key 파생 `detections[].path` 필드를 통해 audit 로그에 도달한 뒤 뷰어에서 렌더됨 | 제공 페이지는 DOM을 `createElement` + `textContent`로만 구성(`innerHTML` 보간 사용 안 함)하고, 모든 응답에 `require-trusted-types-for 'script'`를 포함한 엄격 CSP를 부여(잔여 `innerHTML` sink는 브라우저에서 throw). allowlist는 필드 *이름*을, CSP + `textContent`는 악의적 *값*을 각각 독립적으로 무력화. 실질적 잔여 없음 |
|
|
60
|
+
| 뷰어를 통한 audit 필드 leak (미래 필드) (0.9) | 이후 audit 스키마에 추가된 필드가 dashboard API에 그대로 노출되어 의도치 않은 메타데이터 유출 | `/api/events`는 실제 audit 스키마에 대해 **재귀적 key-by-key 필드 allowlist projection**을 수행(`detections`/`identity`/`summary`/`auditIntegrity` 같은 중첩 sub-object를 blind하게 spread하지 않음)하며 core의 `FORBIDDEN_KEYS` 위에 적층됨. 어떤 레벨의 새 중첩 필드든 기본적으로 drop |
|
|
61
|
+
| localhost bind 뷰어에 대한 DNS-rebinding audit JSON 읽기 (0.9) | 운영자가 방문한 사이트가 `127.0.0.1`로 재해석되는 단기 TTL DNS 이름을 게시하면 피해자 브라우저가 same-origin 요청을 보내 공격자 JS가 인증 없는 loopback dashboard에서 audit JSON을 읽음 | 요청별 **anti-rebinding `Host` 헤더 allowlist**(bind 검사와 구분되는 first gate; IPv4-mapped IPv6·trailing-dot·bracketed IPv6 정규화, malformed/중복 `Host` 거부) + `Cross-Origin-Resource-Policy`/`Cross-Origin-Opener-Policy: same-origin`; CORS 헤더는 결코 방출하지 않음. 실질적 잔여 없음 |
|
|
62
|
+
| remote bind 시 인증 없는 audit 읽기 (0.9) | non-loopback host에 bind된 dashboard가 로그인 없이 audit 스트림 노출 | fail-closed precedence: `allowRemoteBind` **및** `sessionGuard`가 있고 **확인된 HTTPS 종단**(`tlsContext`, 또는 신뢰 proxy에서만 `X-Forwarded-Proto`를 신뢰하는 `trustProxy`)이 아니면 remote bind는 throw; Secure/`__Host-` 세션 쿠키는 평문 http로 전송되지 않음. 운영자가 TLS 종단을 책임 |
|
|
63
|
+
| OIDC login CSRF / authorization-code injection / open-redirect / session fixation (0.9) | 공격자가 피해자 broker 세션을 공격자 제어 로그인에 강제하거나, 탈취한 code를 주입하거나, 로그인 후 off-origin으로 redirect하거나, 사전 인지된 세션 id를 고정 | `/auth/callback`은 **state-first**: pre-auth 쿠키 바인딩된 pending record를 atomic `take()`하고 **모든 IdP egress 이전에** constant-time `state` 비교; PKCE S256 필수; callback에서 **새 세션 id 발급**(fixation 없음, pre-auth 쿠키 폐기); 로그인 후 `return_to`는 상대 경로 `returnToAllowlist`로 검증; logout은 non-GET + CSRF-header gated. 단일 IdP 기준 실질적 잔여 없음 |
|
|
64
|
+
| OIDC mix-up (잘못된 IdP / 잘못된 RP) (0.9) | confused-deputy 공격으로 IdP를 바꾸거나 다른 client용으로 발급된 code/token을 재생 | issuer/`token_endpoint`/`jwks_uri`를 `/auth/login`에서 pending record에 pin; RFC 9207 `iss` 응답 파라미터가 pinned issuer와 일치해야 함; `metadata.issuer`가 설정 issuer와 string-equal해야 함; OIDC ID-token `aud`/`azp` 프로파일(`aud`는 `clientId` 포함; multi-valued `aud`는 `azp === clientId` 필요)로 cross-client 차단. multi-origin IdP는 범위 외 |
|
|
65
|
+
| 토큰 엔드포인트 POST(및 Vault `fetch`)를 통한 broker SSRF — cloud metadata (0.9) | discovery와 request 사이에 `169.254.169.254`로 DNS-rebind되는 `token_endpoint`(또는 운영자 제공 `VAULT_ADDR`)가 instance-metadata 자격증명을 유출 | 모든 egress(discovery GET, 공유 verifier 경유 JWKS GET, token-exchange POST, end-session redirect, `haechi-crypto-kms` Vault `fetch`)가 **request 직전**(post-DNS) `lookup` 후 `isBlockedAddress` 재검사를 `redirect: "error"`·bounded body·timeout과 함께 수행. 운영자 신뢰 엔드포인트에 한함 |
|
|
66
|
+
| audit/로그로의 token/secret leak (broker) (0.9) | ID/access/refresh token, `client_secret`, `code`, `state`, `nonce`, raw `sub`가 audit 로그나 client 응답에 기록됨 | broker는 모든 audit 이벤트를 자체 allowlist로 projection해 `subjectHash`/`issuerHash`/`sessionIdHash`(keyed-HMAC) + `provider`/`reasonCode`/timestamp만 방출; core `FORBIDDEN_KEYS`를 broker token/claim key까지 확장; access token은 **폐기**(저장·사용 안 함). 실질적 잔여 없음 |
|
|
67
|
+
| KMS backend egress (Vault HTTP, GCP/Azure SDK) (0.9) | `haechi-crypto-kms` Vault/GCP/Azure backend가 key material이나 provider/key-path 상세를 유출하거나 의도치 않은 엔드포인트에 도달 | optional-peer + injected-client 모델과 **faithful-mock conformance**(cross-key·corrupted-blob 거부, HMAC determinism/domain-separation); Vault `fetch`는 위 satellite-local SSRF 가드 수행; 모든 backend는 provider 오류를 generic fail-closed 오류로 매핑하고 provider/key-ARN 상세를 audit에 기록하지 않음. live-backend 검증은 CI 외부 |
|
|
57
68
|
|
|
58
69
|
## 4. 명시적 제외
|
|
59
70
|
|
|
@@ -71,6 +82,10 @@ Haechi가 보호하려는 주요 자산은 다음이다.
|
|
|
71
82
|
- URL query string 내 민감값 검사 (JSON body만 검사)
|
|
72
83
|
- 마지막 anchor 이후의 audit tail truncation — `audit.anchor`(0.7)는 anchor가 추가 전용/별도 미디어에 있을 때 마지막 anchor까지의 레코드 삭제를 탐지한다; 마지막 anchor 이후 기록된 레코드와 동일 파일시스템 anchor는 대상에서 제외된다
|
|
73
84
|
- JSON-RPC batch 메시지 처리 (MCP stdio filter는 batch를 fail-closed로 거부)
|
|
85
|
+
- `haechi-auth-oidc`의 multi-origin / CDN-fronted IdP(issuer host ≠ `token_endpoint`/`jwks_uri` host) — single-origin만 지원, `haechi-auth-jwt`와 동일 제약 (0.9)
|
|
86
|
+
- refresh-token rotation / silent renewal / 장수명 broker 세션 — 0.9 세션은 absolute-TTL + idle-timeout만; `offline_access`는 제거되고 access token은 폐기 (0.9)
|
|
87
|
+
- Dashboard write action(reveal, purge, policy edit) — `haechi-dashboard`는 읽기 전용으로 `POST`/`DELETE` surface 없음; mutation은 reveal governance 하의 CLI에 유지 (0.9)
|
|
88
|
+
- OIDC broker의 `at_hash`/`c_hash` 검증 — broker가 access token을 사용하지 않으므로 정확히 범위 외 (0.9)
|
|
74
89
|
|
|
75
90
|
## 5. 남은 운영 전제
|
|
76
91
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
- Status: Draft 0.1
|
|
4
4
|
- Date: 2026-06-10
|
|
5
|
-
- Target version: 0.
|
|
5
|
+
- Target version: 0.9.0
|
|
6
6
|
|
|
7
7
|
## 1. Assets Under Protection
|
|
8
8
|
|
|
@@ -54,6 +54,17 @@ The primary assets Haechi protects are:
|
|
|
54
54
|
| Raw credentials/identity in audit | Token or subject leak through the audit log | Tokens stored only as keyed-HMAC hashes; identity subject/issuer are keyed HMAC; `auth_denied` records no token |
|
|
55
55
|
| Token round-trip restoring foreign tokens | Cross-client/request plaintext recovery | Detokenization is opt-in (`detokenizeResponses`) and request-scoped: only tokens issued while protecting the same request are restored |
|
|
56
56
|
| Indirect prompt injection in tool results/responses | Agent manipulation via planted instructions | Response-direction heuristics, report-only by default (`injection` action `allow`); escalation is an explicit policy choice. Not a complete defense |
|
|
57
|
+
| Haechi re-detecting its own transform markers | A tokenized round-trip echoed by the model is re-flagged (e.g. `[TOKEN:…]` blocked as a `secret`), breaking `detokenizeResponses` under response-enforce | **Response direction only**, detection skips Haechi markers (`[TOKEN:…]`, `[HAECHI_ENC:…]`, `[REDACTED:…]`). Request-direction is unaffected, so a marker-shaped string in a request can't smuggle a secret past detection. **Accepted residual:** a hostile *upstream* could wrap a leaked value in a fake response marker to dodge response-direction detection — response inspection is a secondary defense (the model is semi-trusted) and the exclusion is positional (only the marker span is skipped; adjacent values are still detected) |
|
|
58
|
+
| Response-metadata false positives | Response inspection in `enforce` blocks legitimate completions because envelope metadata looks PII/secret-shaped (e.g. a unix-timestamp `created` matching the phone rule, a nanosecond `*_duration` matching `card`) | The KR phone rule ignores bare separator-less non-`0`-led digit runs; ids like `chatcmpl-…` are not secret-shaped; and the **response direction does not scan bare JSON number leaves** (`*_duration`/count/timestamp/numeric-id — never a model-leaked card/RRN; a real leak lands in generated *text*, still inspected). **Accepted residual:** a hostile model could exfiltrate a value encoded as a bare response number (response inspection is a secondary defense) — a strict deployment can opt back in with `responseProtection.scanNumbers: true`. Real vLLM + Ollama responses now scan clean; for extra caution use `responseProtection.mode: report-only` (detect + audit, no block) |
|
|
59
|
+
| Dashboard audit-viewer XSS via attacker-controlled `detections[].path` (0.9) | Stored XSS in `haechi-dashboard`: a request JSON key like `<img onerror>` reaches the audit log via the client-key-derived `detections[].path` field, then renders in the viewer | The served page builds DOM with `createElement` + `textContent` only (never `innerHTML` interpolation), and every response carries a strict CSP including `require-trusted-types-for 'script'` (any stray `innerHTML` sink throws in-browser). The allowlist bounds field *names* and CSP + `textContent` neutralize malicious *values* — independent layers. None material |
|
|
60
|
+
| Audit field leak via the viewer (future field) (0.9) | A field added to the audit schema later is surfaced verbatim by the dashboard API, leaking metadata never meant to be exposed | `/api/events` runs a **recursive, key-by-key field allowlist projection** against the real audit schema (no nested sub-object — `detections`/`identity`/`summary`/`auditIntegrity` — is spread through blind), layered over core's `FORBIDDEN_KEYS`. A new nested field at any level defaults to dropped |
|
|
61
|
+
| DNS-rebinding read of audit JSON from a localhost-bound viewer (0.9) | A site the operator browses publishes a short-TTL DNS name re-resolving to `127.0.0.1`, so the victim's browser makes same-origin requests and the attacker's JS reads the audit JSON from an unauthenticated loopback dashboard | Per-request **anti-rebinding `Host`-header allowlist** (first gate, distinct from the bind check; normalizes IPv4-mapped IPv6, trailing-dot, bracketed IPv6, rejects malformed/duplicate `Host`) plus `Cross-Origin-Resource-Policy`/`Cross-Origin-Opener-Policy: same-origin`; CORS headers are never emitted. None material |
|
|
62
|
+
| Unauthenticated audit read on remote bind (0.9) | The dashboard bound to a non-loopback host exposes the audit stream with no login | Fail-closed precedence: remote bind throws unless `allowRemoteBind` **and** a `sessionGuard` are present **and** confirmed HTTPS termination (a `tlsContext`, or `trustProxy` honoring `X-Forwarded-Proto` only from a trusted proxy); a Secure/`__Host-` session cookie is never sent over plaintext http. Operator must terminate TLS |
|
|
63
|
+
| OIDC login CSRF / authorization-code injection / open-redirect / session fixation (0.9) | An attacker forces a victim's broker session onto an attacker-controlled login, injects a stolen code, redirects post-login off-origin, or fixes a pre-known session id | `/auth/callback` is **state-first**: atomic `take()` of a pre-auth-cookie-bound pending record and constant-time `state` compare **before any IdP egress**; PKCE S256 mandatory; a **fresh session id minted at callback** (no fixation, pre-auth cookie discarded); post-login `return_to` validated against a relative-path `returnToAllowlist`; logout is non-GET + CSRF-header gated. None material for a single IdP |
|
|
64
|
+
| OIDC mix-up (wrong IdP / wrong RP) (0.9) | A confused-deputy attack swaps the IdP or replays a code/token minted for a different client | Issuer/`token_endpoint`/`jwks_uri` are pinned into the pending record at `/auth/login`; the RFC 9207 `iss` response param must equal the pinned issuer; `metadata.issuer` must string-equal the configured issuer; and the OIDC ID-token `aud`/`azp` profile (`aud` must contain `clientId`; multi-valued `aud` requires `azp === clientId`) closes cross-client. Multi-origin IdP out of scope |
|
|
65
|
+
| Broker SSRF to cloud metadata via the token-endpoint POST (and Vault `fetch`) (0.9) | A `token_endpoint` (or operator-supplied `VAULT_ADDR`) that DNS-rebinds to `169.254.169.254` between discovery and request exfiltrates instance-metadata credentials | Every egress (discovery GET, JWKS GET via the shared verifier, token-exchange POST, end-session redirect, and the `haechi-crypto-kms` Vault `fetch`) runs a `lookup`-then-`isBlockedAddress` re-check **immediately before the request** (post-DNS), with `redirect: "error"`, a bounded response body, and a timeout. Operator-trusted endpoints only |
|
|
66
|
+
| Token/secret leak into audit/logs (broker) (0.9) | An ID/access/refresh token, `client_secret`, `code`, `state`, `nonce`, or raw `sub` is written to the audit log or a client response | The broker projects every audit event through its own allowlist and emits only `subjectHash`/`issuerHash`/`sessionIdHash` (keyed-HMAC) + `provider`/`reasonCode`/timestamp; core's `FORBIDDEN_KEYS` is extended to cover the broker token/claim keys; the access token is **discarded** (never stored or used). None material |
|
|
67
|
+
| KMS backend egress (Vault HTTP, GCP/Azure SDK) (0.9) | A `haechi-crypto-kms` Vault/GCP/Azure backend leaks key material or provider/key-path detail, or reaches an unintended endpoint | Optional-peer + injected-client model with **faithful-mock conformance** (cross-key + corrupted-blob rejection, HMAC determinism/domain-separation); the Vault `fetch` runs the satellite-local SSRF guard above; all backends map provider errors to a generic fail-closed error and never write provider/key-ARN detail to audit. Live-backend validation is out of CI |
|
|
57
68
|
|
|
58
69
|
## 4. Explicit Exclusions
|
|
59
70
|
|
|
@@ -71,6 +82,10 @@ The primary assets Haechi protects are:
|
|
|
71
82
|
- Detection of sensitive values in URL query strings (JSON body only)
|
|
72
83
|
- Audit tail truncation beyond the last anchor — `audit.anchor` (0.7) detects deletion of records back to the last anchor when the anchor is on append-only/separate media; records written after the last anchor, and same-filesystem anchors, are not covered
|
|
73
84
|
- JSON-RPC batch message processing (the MCP stdio filter rejects batches fail-closed)
|
|
85
|
+
- Multi-origin / CDN-fronted IdP for `haechi-auth-oidc` (issuer host ≠ `token_endpoint`/`jwks_uri` host) — single-origin only, same constraint as `haechi-auth-jwt` (0.9)
|
|
86
|
+
- Refresh-token rotation / silent renewal / long-lived broker sessions — 0.9 sessions are absolute-TTL + idle-timeout only; `offline_access` is stripped and the access token is discarded (0.9)
|
|
87
|
+
- Dashboard write actions (reveal, purge, policy edits) — `haechi-dashboard` is read-only with no `POST`/`DELETE` surface; mutation stays in the CLI under reveal governance (0.9)
|
|
88
|
+
- `at_hash`/`c_hash` validation in the OIDC broker — out of scope precisely because the broker never uses the access token (0.9)
|
|
74
89
|
|
|
75
90
|
## 5. Remaining Operational Assumptions
|
|
76
91
|
|
package/package.json
CHANGED
package/packages/audit/index.mjs
CHANGED
|
@@ -5,7 +5,18 @@ import { dirname } from "node:path";
|
|
|
5
5
|
import { createInterface } from "node:readline";
|
|
6
6
|
import { setTimeout as delay } from "node:timers/promises";
|
|
7
7
|
|
|
8
|
-
const FORBIDDEN_KEYS = new Set([
|
|
8
|
+
const FORBIDDEN_KEYS = new Set([
|
|
9
|
+
"value", "plaintext", "payload", "content", "message", "prompt", "secret",
|
|
10
|
+
// OIDC-broker / OAuth token, secret, and authorization-flow parameter keys.
|
|
11
|
+
// These are never part of a current audit event shape; the membership is a
|
|
12
|
+
// defense-in-depth guard so a future audit field can never leak a token,
|
|
13
|
+
// client secret, or flow parameter through the core sink. `sub`/`email` are
|
|
14
|
+
// intentionally NOT listed — they can be legitimate non-secret field names
|
|
15
|
+
// elsewhere, and the broker already self-guards them via its own allowlist
|
|
16
|
+
// projection.
|
|
17
|
+
"access_token", "id_token", "refresh_token", "code", "code_verifier",
|
|
18
|
+
"client_secret", "state", "nonce"
|
|
19
|
+
]);
|
|
9
20
|
|
|
10
21
|
export function createJsonlAuditSink({ path, anchor = null }) {
|
|
11
22
|
if (!path) {
|
package/packages/cli/runtime.mjs
CHANGED
|
@@ -32,7 +32,8 @@ export function defaultConfig() {
|
|
|
32
32
|
failureMode: "fail-closed",
|
|
33
33
|
allowNonJson: false,
|
|
34
34
|
allowCompressed: false,
|
|
35
|
-
maxBytes: 1048576
|
|
35
|
+
maxBytes: 1048576,
|
|
36
|
+
scanNumbers: false
|
|
36
37
|
},
|
|
37
38
|
streaming: {
|
|
38
39
|
requestMode: "block",
|
|
@@ -320,6 +321,9 @@ export function normalizeConfig(config) {
|
|
|
320
321
|
if (typeof merged.responseProtection.maxBytes !== "number" || merged.responseProtection.maxBytes < 1) {
|
|
321
322
|
throw new Error("responseProtection.maxBytes must be a positive number");
|
|
322
323
|
}
|
|
324
|
+
if (typeof merged.responseProtection.scanNumbers !== "boolean") {
|
|
325
|
+
throw new Error("responseProtection.scanNumbers must be boolean");
|
|
326
|
+
}
|
|
323
327
|
if (!["block", "pass-through", "inspect"].includes(merged.streaming.requestMode)) {
|
|
324
328
|
throw new Error(`Invalid streaming.requestMode: ${merged.streaming.requestMode}`);
|
|
325
329
|
}
|
package/packages/core/index.mjs
CHANGED
|
@@ -15,6 +15,10 @@ export function createHaechi({ filterEngine, policyEngine, cryptoProvider, audit
|
|
|
15
15
|
const effectiveMode = context.mode ?? mode;
|
|
16
16
|
const engine = contextEngine ?? policyEngine;
|
|
17
17
|
const entries = collectStringEntries(payload);
|
|
18
|
+
// `context` is threaded into detection as-is and is LOAD-BEARING: e.g.
|
|
19
|
+
// `context.direction` ("request" | "response") gates direction-scoped rules
|
|
20
|
+
// (injection) and the response-only marker exclusion in the filter engine.
|
|
21
|
+
// The proxy sets it per direction; do not drop it here.
|
|
18
22
|
const detections = await filterEngine.detect({ entries, context });
|
|
19
23
|
const decisions = [];
|
|
20
24
|
|
|
@@ -7,11 +7,14 @@ const DEFAULT_RULES = [
|
|
|
7
7
|
confidence: 0.95
|
|
8
8
|
},
|
|
9
9
|
{
|
|
10
|
+
// KR mobile numbers (01[016789] prefixes); landlines are out of scope.
|
|
11
|
+
// krPhoneValid keeps a bare separator-less run from matching a timestamp/id.
|
|
10
12
|
id: "kr-phone",
|
|
11
13
|
type: "phone",
|
|
12
14
|
pattern: "(?:\\+82[-\\s]?)?0?1[016789][-.\\s]?\\d{3,4}[-.\\s]?\\d{4}",
|
|
13
15
|
flags: "g",
|
|
14
|
-
confidence: 0.9
|
|
16
|
+
confidence: 0.9,
|
|
17
|
+
validate: krPhoneValid
|
|
15
18
|
},
|
|
16
19
|
{
|
|
17
20
|
id: "kr-rrn-like",
|
|
@@ -116,6 +119,27 @@ export function createDefaultFilterEngine({ customRules = [] } = {}) {
|
|
|
116
119
|
|
|
117
120
|
export function detectEntry(entry, rules, context = {}) {
|
|
118
121
|
const detections = [];
|
|
122
|
+
// On the RESPONSE direction, a bare JSON NUMBER leaf is inference-server
|
|
123
|
+
// metadata (a nanosecond `*_duration`, a token count, a numeric id/timestamp) —
|
|
124
|
+
// never a model-leaked card/phone/RRN. Scanning it only yields false positives:
|
|
125
|
+
// a long Luhn-passing duration matches `card`, a 13-digit one matches `kr_rrn`.
|
|
126
|
+
// The REQUEST direction still scans numbers (a client CAN send a card as a
|
|
127
|
+
// number); model-leaked PII lands in generated TEXT (string leaves), which are
|
|
128
|
+
// still inspected. (Accepted residual: a hostile model could exfiltrate a value
|
|
129
|
+
// as a bare response number — response inspection is a secondary defense.) A
|
|
130
|
+
// strict deployment can opt back in with `responseProtection.scanNumbers: true`
|
|
131
|
+
// (threaded as context.scanNumbers), accepting the metadata false positives.
|
|
132
|
+
if (context?.direction === "response" && entry.kind === "number" && !context?.scanNumbers) {
|
|
133
|
+
return detections;
|
|
134
|
+
}
|
|
135
|
+
// On the RESPONSE direction only, skip Haechi's own transform markers so they
|
|
136
|
+
// aren't re-detected: a tokenized round-trip echoes `[TOKEN:tok_…]` back, which
|
|
137
|
+
// reads like a `token:<secret>` assignment — without this, Haechi blocks its
|
|
138
|
+
// own token. This is response-only on purpose: a REQUEST that contains a
|
|
139
|
+
// marker-shaped string is NOT Haechi output (Haechi hasn't transformed it yet),
|
|
140
|
+
// so it is scanned normally — otherwise an attacker could wrap a real secret in
|
|
141
|
+
// a fake `[TOKEN:…]` to evade request-side detection.
|
|
142
|
+
const markerSpans = context?.direction === "response" ? haechiMarkerSpans(entry.value) : [];
|
|
119
143
|
|
|
120
144
|
for (const rule of rules) {
|
|
121
145
|
// Direction-scoped rules (e.g. injection heuristics) only run on the
|
|
@@ -129,14 +153,19 @@ export function detectEntry(entry, rules, context = {}) {
|
|
|
129
153
|
if (rule.validate && !rule.validate(value)) {
|
|
130
154
|
continue;
|
|
131
155
|
}
|
|
156
|
+
const start = match.index;
|
|
157
|
+
const end = match.index + value.length;
|
|
158
|
+
if (overlapsAny(start, end, markerSpans)) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
132
161
|
detections.push({
|
|
133
162
|
type: rule.type,
|
|
134
163
|
ruleId: rule.id,
|
|
135
164
|
path: entry.path,
|
|
136
165
|
pathText: entry.pathText,
|
|
137
166
|
kind: entry.kind ?? "value",
|
|
138
|
-
start
|
|
139
|
-
end
|
|
167
|
+
start,
|
|
168
|
+
end,
|
|
140
169
|
confidence: rule.confidence,
|
|
141
170
|
value
|
|
142
171
|
});
|
|
@@ -146,6 +175,32 @@ export function detectEntry(entry, rules, context = {}) {
|
|
|
146
175
|
return removeOverlaps(detections);
|
|
147
176
|
}
|
|
148
177
|
|
|
178
|
+
// Spans of Haechi's own transform markers in a string, so detection can skip
|
|
179
|
+
// them: `[TOKEN:…]`, `[HAECHI_ENC:…]`, `[REDACTED:…]`.
|
|
180
|
+
function haechiMarkerSpans(text) {
|
|
181
|
+
const spans = [];
|
|
182
|
+
for (const m of text.matchAll(/\[(?:TOKEN|HAECHI_ENC|REDACTED):[^\]]*\]/g)) {
|
|
183
|
+
spans.push([m.index, m.index + m[0].length]);
|
|
184
|
+
}
|
|
185
|
+
return spans;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function overlapsAny(start, end, spans) {
|
|
189
|
+
return spans.some(([s, e]) => start < e && end > s);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// A bare digit run with no separators and no +82 country code is only treated as
|
|
193
|
+
// a KR phone number when it starts with the trunk prefix 0 (e.g. 01012345678);
|
|
194
|
+
// otherwise an ambiguous 10-digit value (a unix timestamp, an id, a counter)
|
|
195
|
+
// merely looks phone-shaped. Separated/prefixed forms (010-1234-5678,
|
|
196
|
+
// +82 10 1234 5678) always pass.
|
|
197
|
+
function krPhoneValid(match) {
|
|
198
|
+
if (/[-.\s+]/.test(match)) {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
return match.startsWith("0");
|
|
202
|
+
}
|
|
203
|
+
|
|
149
204
|
function normalizeCustomRule(rule) {
|
|
150
205
|
if (!rule.id || !rule.type || !rule.pattern) {
|
|
151
206
|
throw new Error("Custom filter rule requires id, type, and pattern");
|
package/packages/proxy/index.mjs
CHANGED
|
@@ -438,6 +438,9 @@ async function maybeProtectResponse({ upstreamResponse, routeContext, runtime, a
|
|
|
438
438
|
...authContext,
|
|
439
439
|
operation: `response:${routeContext.operation}`,
|
|
440
440
|
direction: "response",
|
|
441
|
+
// Opt-in: scan bare number leaves on the response (off by default — they are
|
|
442
|
+
// inference-server metadata; see the filter engine's number-leaf skip).
|
|
443
|
+
scanNumbers: runtime.config.responseProtection.scanNumbers,
|
|
441
444
|
mode: runtime.config.responseProtection.mode ?? runtime.config.policy.mode ?? runtime.config.mode
|
|
442
445
|
});
|
|
443
446
|
|