mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(security): decouple request PII redaction from injection mode (#8102)
* fix(security): decouple request PII redaction from injection mode PII_REDACTION_ENABLED now rewrites request PII independently of INPUT_SANITIZER_MODE, so the enterprise recipe (MODE=block + PII on) actually redacts. Also cover Responses API string input/prompt shapes and correct docs that claimed MODE=redact strips injection text. Refs: #8092 #8093 #8094 #8096 #8097 * test(security): drop no-explicit-any in sanitizer unit tests Unblocks CI lint/quality ratchet on the PII redaction PR by typing chat-like payloads instead of casting to any. --------- Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
This commit is contained in:
@@ -353,7 +353,7 @@ ALLOW_API_KEY_REVEAL=false
|
||||
# Scans incoming messages for prompt injection patterns before routing.
|
||||
# Used by: src/middleware/promptInjectionGuard.ts
|
||||
# INPUT_SANITIZER_ENABLED=true
|
||||
# INPUT_SANITIZER_MODE=warn # warn = log only | block = reject request | redact = strip patterns
|
||||
# INPUT_SANITIZER_MODE=warn # warn = log only | block = reject request | redact = legacy (does NOT strip injection; use PII_REDACTION_ENABLED for request PII)
|
||||
|
||||
# Legacy alias for INPUT_SANITIZER_MODE (same effect).
|
||||
# INJECTION_GUARD_MODE=warn
|
||||
|
||||
@@ -91,7 +91,7 @@ Configure via dashboard (Settings → Security) or `.env`:
|
||||
|
||||
```env
|
||||
INPUT_SANITIZER_ENABLED=true
|
||||
INPUT_SANITIZER_MODE=block # warn | block | redact
|
||||
INPUT_SANITIZER_MODE=block # warn | block (injection policy; legacy "redact" does not strip injection text)
|
||||
```
|
||||
|
||||
### 🔒 PII Redaction
|
||||
@@ -108,7 +108,8 @@ Automatic detection and optional redaction of personally identifiable informatio
|
||||
| SSN (US) | `123-45-6789` | `[SSN_REDACTED]` |
|
||||
|
||||
```env
|
||||
PII_REDACTION_ENABLED=true
|
||||
PII_REDACTION_ENABLED=true # request PII rewrite; independent of INPUT_SANITIZER_MODE
|
||||
PII_RESPONSE_SANITIZATION=true # optional: redact PII in provider responses returned to clients
|
||||
```
|
||||
|
||||
### 🌐 Network Security
|
||||
|
||||
@@ -218,9 +218,9 @@ OmniRoute provides a two-layer defense: request-side injection scanning and resp
|
||||
| Variable | Default | Source File | Description |
|
||||
| ------------------------- | --------- | ---------------------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| `INPUT_SANITIZER_ENABLED` | `true` | `src/middleware/promptInjectionGuard.ts` | Enable scanning of incoming messages for prompt injection patterns. |
|
||||
| `INPUT_SANITIZER_MODE` | `warn` | `src/middleware/promptInjectionGuard.ts` | `warn` = log only, `block` = reject request with 400, `redact` = strip suspicious patterns. |
|
||||
| `INPUT_SANITIZER_MODE` | `warn` | `src/middleware/promptInjectionGuard.ts` | Injection policy: `warn` = log only, `block` = reject request with 400. Legacy `redact` does **not** strip injection text; use `PII_REDACTION_ENABLED` for request PII rewrite. |
|
||||
| `INJECTION_GUARD_MODE` | _(unset)_ | `src/middleware/promptInjectionGuard.ts` | Legacy alias for `INPUT_SANITIZER_MODE` — same behavior. |
|
||||
| `PII_REDACTION_ENABLED` | `false` | `src/middleware/promptInjectionGuard.ts` | Detect PII (emails, phones, SSNs) in incoming requests. |
|
||||
| `PII_REDACTION_ENABLED` | `false` | `src/lib/guardrails/piiMasker.ts` | When `true`, redact PII in incoming requests (independent of injection mode). |
|
||||
| `CREDENTIAL_REDACTION_ENABLED` | `false` | `src/lib/guardrails/credentialMasker.ts` | Redact well-known API-key / secret-token patterns from request/response payloads. Opt-in; mirrors `PII_REDACTION_ENABLED`. |
|
||||
|
||||
### Response-Side: PII Sanitizer
|
||||
@@ -240,7 +240,7 @@ OmniRoute provides a two-layer defense: request-side injection scanning and resp
|
||||
|
||||
| Scenario | Configuration |
|
||||
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Enterprise compliance** | `INPUT_SANITIZER_ENABLED=true`, `INPUT_SANITIZER_MODE=block`, `PII_REDACTION_ENABLED=true`, `PII_RESPONSE_SANITIZATION=true` |
|
||||
| **Enterprise compliance** | `INPUT_SANITIZER_ENABLED=true`, `INPUT_SANITIZER_MODE=block`, `PII_REDACTION_ENABLED=true`, `PII_RESPONSE_SANITIZATION=true` (injection blocks + request/response PII redaction; modes are independent) |
|
||||
| **Monitoring only** | `INPUT_SANITIZER_ENABLED=true`, `INPUT_SANITIZER_MODE=warn` — logs but never blocks |
|
||||
| **Personal use** | Leave all disabled — zero overhead |
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ used when neither a DB override nor an environment variable is present.
|
||||
| `REQUIRE_API_KEY` | boolean | `false` | Require an API key for all incoming requests. |
|
||||
| `INPUT_SANITIZER_ENABLED` | boolean | `true` | Enable input sanitization for all requests. |
|
||||
| `INJECTION_GUARD_MODE` | enum | `off` | Prompt injection guard mode. Values: `off`, `warn`, `block`, `redact`. |
|
||||
| `PII_REDACTION_ENABLED` | boolean | `false` | Redact personally identifiable information from requests. |
|
||||
| `PII_REDACTION_ENABLED` | boolean | `false` | Redact PII from requests (independent of `INPUT_SANITIZER_MODE`). |
|
||||
| `PII_RESPONSE_SANITIZATION` | boolean | `false` | Sanitize PII from provider responses. |
|
||||
| `PII_RESPONSE_SANITIZATION_MODE` | enum | `redact` | Mode for PII response sanitization. Values: `redact`, `warn`, `block`, `off`. |
|
||||
| `OUTBOUND_SSRF_GUARD_ENABLED` | boolean | `true` | Block outbound requests to private/internal IP ranges. |
|
||||
|
||||
@@ -60,12 +60,13 @@ exposes a `deps` constructor option so tests can inject fake `getSettings` and
|
||||
|
||||
Runs on **both** stages.
|
||||
|
||||
- **`preCall`** clones the payload, walks `system`, `messages`, and `input`
|
||||
arrays, and applies `processPII()` (from `@/shared/utils/inputSanitizer`) to
|
||||
string `content`/`text` fields. When `PII_REDACTION_ENABLED=true` **and**
|
||||
`INPUT_SANITIZER_MODE=redact`, detected PII is stripped/redacted in the
|
||||
outbound payload. Otherwise the call records detection counts without
|
||||
rewriting content.
|
||||
- **`preCall`** clones the payload, walks `system`, `messages`, `input`, and
|
||||
`prompt` (including plain string items), and applies `processPII()` (from
|
||||
`@/shared/utils/inputSanitizer`) to string `content`/`text` fields. When
|
||||
`PII_REDACTION_ENABLED=true`, detected PII is redacted in the outbound
|
||||
payload. This is independent of `INPUT_SANITIZER_MODE` (which only controls
|
||||
prompt-injection policy). When redaction is off, the call records detection
|
||||
counts without rewriting content.
|
||||
- **`postCall`** deep-clones the response, runs `sanitizePIIResponse()` plus
|
||||
the Responses-API-shape masker (`maskResponsesOutput` — covers
|
||||
`output_text` and `output[].content[].text`). If any redaction occurs, the
|
||||
@@ -83,7 +84,7 @@ options:
|
||||
| Setting | Env var | Default | Effect |
|
||||
| --------------- | ----------------------------------------------- | ------- | --------------------------------------- |
|
||||
| Enabled | `INPUT_SANITIZER_ENABLED` | `true` | When `false`, guardrail short-circuits. |
|
||||
| Mode | `INJECTION_GUARD_MODE` / `INPUT_SANITIZER_MODE` | `warn` | `block`, `warn`, or `log`. |
|
||||
| Mode | `INJECTION_GUARD_MODE` / `INPUT_SANITIZER_MODE` | `warn` | Injection policy: `block`, `warn`, or `log`. (`redact` is accepted for back-compat but does **not** strip injection text; request PII rewrite is controlled by `PII_REDACTION_ENABLED`.) |
|
||||
| Block threshold | `blockThreshold` option | `high` | Minimum severity required to block. |
|
||||
|
||||
**Mode precedence** (`getMode`): caller `options.mode` →
|
||||
@@ -223,9 +224,9 @@ Environment variables read by the built-in guardrails:
|
||||
| Variable | Used by | Effect |
|
||||
| ------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| `INPUT_SANITIZER_ENABLED` | `prompt-injection` | Set `false` to disable detection entirely. |
|
||||
| `INPUT_SANITIZER_MODE` | `prompt-injection`, `pii-masker` | Shared mode: `warn`, `block`, `log`, or `redact`. |
|
||||
| `INPUT_SANITIZER_MODE` | `prompt-injection` | Injection policy: `warn`, `block`, or `log`. Legacy value `redact` does not rewrite injection text. |
|
||||
| `INJECTION_GUARD_MODE` | `prompt-injection` | Mode for the injection guard; also a DB feature flag that **overrides** the env vars (DB > ENV). |
|
||||
| `PII_REDACTION_ENABLED` | `pii-masker` | When `true` + mode `redact`, request PII is stripped. |
|
||||
| `PII_REDACTION_ENABLED` | `pii-masker` | When `true`, request PII is redacted (independent of injection mode). |
|
||||
| `PII_RESPONSE_SANITIZATION` / `_MODE` | `pii-masker` (downstream) | Controls response-side masker behavior. |
|
||||
|
||||
The Vision Bridge reads runtime config from the DB-backed settings store
|
||||
|
||||
@@ -10,9 +10,9 @@ type PiiDetection = {
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
function isRequestPiiMaskingEnabled() {
|
||||
return (
|
||||
process.env.PII_REDACTION_ENABLED === "true" && process.env.INPUT_SANITIZER_MODE === "redact"
|
||||
);
|
||||
// Request PII redaction is controlled solely by PII_REDACTION_ENABLED.
|
||||
// INPUT_SANITIZER_MODE only governs prompt-injection policy (warn/block/log).
|
||||
return process.env.PII_REDACTION_ENABLED === "true";
|
||||
}
|
||||
|
||||
function sanitizeStringValue(text: string) {
|
||||
@@ -84,6 +84,13 @@ function cloneAndMaskRequestPayload(payload: unknown) {
|
||||
const sanitizeMessageLikeList = (list: unknown) => {
|
||||
if (!Array.isArray(list)) return list;
|
||||
return list.map((entry) => {
|
||||
// Responses API can pass plain strings in input[]
|
||||
if (typeof entry === "string") {
|
||||
const result = sanitizeStringValue(entry);
|
||||
detections.push(...result.detections);
|
||||
modified ||= result.modified;
|
||||
return result.text;
|
||||
}
|
||||
if (!entry || typeof entry !== "object") return entry;
|
||||
const record = { ...(entry as JsonRecord) };
|
||||
if ("content" in record) {
|
||||
@@ -116,6 +123,20 @@ function cloneAndMaskRequestPayload(payload: unknown) {
|
||||
|
||||
if (Array.isArray(clonedPayload.input)) {
|
||||
clonedPayload.input = sanitizeMessageLikeList(clonedPayload.input);
|
||||
} else if (typeof clonedPayload.input === "string") {
|
||||
const result = sanitizeStringValue(clonedPayload.input);
|
||||
detections.push(...result.detections);
|
||||
modified ||= result.modified;
|
||||
clonedPayload.input = result.text;
|
||||
}
|
||||
|
||||
if (typeof clonedPayload.prompt === "string") {
|
||||
const result = sanitizeStringValue(clonedPayload.prompt);
|
||||
detections.push(...result.detections);
|
||||
modified ||= result.modified;
|
||||
clonedPayload.prompt = result.text;
|
||||
} else if (Array.isArray(clonedPayload.prompt)) {
|
||||
clonedPayload.prompt = sanitizeMessageLikeList(clonedPayload.prompt);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -278,8 +278,10 @@ export function sanitizeRequest(body, logger = console) {
|
||||
}
|
||||
|
||||
// ── PII Detection / Redaction ──
|
||||
// PII rewrite is controlled by PII_REDACTION_ENABLED only.
|
||||
// INPUT_SANITIZER_MODE is reserved for prompt-injection policy (warn/block/log).
|
||||
if (config.piiRedaction) {
|
||||
const piiResult = processPII(fullText, config.mode === "redact");
|
||||
const piiResult = processPII(fullText, true);
|
||||
result.piiDetections = piiResult.detections;
|
||||
|
||||
if (piiResult.detections.length > 0) {
|
||||
@@ -287,11 +289,9 @@ export function sanitizeRequest(body, logger = console) {
|
||||
`[SANITIZER] PII detected: ${piiResult.detections.map((d) => `${d.type}(${d.count})`).join(", ")}`
|
||||
);
|
||||
|
||||
if (config.mode === "redact") {
|
||||
// Deep clone and replace message contents with redacted versions
|
||||
result.sanitizedBody = redactBody(body);
|
||||
result.modified = true;
|
||||
}
|
||||
// Deep clone and replace message contents with redacted versions
|
||||
result.sanitizedBody = redactBody(body);
|
||||
result.modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,36 +304,90 @@ export function sanitizeRequest(body, logger = console) {
|
||||
* @returns {Object}
|
||||
*/
|
||||
function redactBody(body) {
|
||||
// Deep clone to avoid mutating original
|
||||
const clone = JSON.parse(JSON.stringify(body));
|
||||
const messageSource = clone.messages !== undefined ? clone.messages : clone.input;
|
||||
const messages = Array.isArray(messageSource)
|
||||
? messageSource
|
||||
: messageSource && typeof messageSource === "object"
|
||||
? [messageSource]
|
||||
: [];
|
||||
: messageSource === undefined || messageSource === null
|
||||
? []
|
||||
: [messageSource];
|
||||
|
||||
for (const msg of messages) {
|
||||
if (typeof msg.content === "string") {
|
||||
msg.content = processPII(msg.content, true).text;
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
for (const part of msg.content) {
|
||||
if (typeof part === "string") {
|
||||
const idx = msg.content.indexOf(part);
|
||||
msg.content[idx] = processPII(part, true).text;
|
||||
} else if (part.text) {
|
||||
part.text = processPII(part.text, true).text;
|
||||
}
|
||||
}
|
||||
const redactContentValue = (value) => {
|
||||
if (typeof value === "string") {
|
||||
return processPII(value, true).text;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((part) => {
|
||||
if (typeof part === "string") {
|
||||
return processPII(part, true).text;
|
||||
}
|
||||
if (part && typeof part === "object") {
|
||||
const next = { ...part };
|
||||
if (typeof next.text === "string") {
|
||||
next.text = processPII(next.text, true).text;
|
||||
}
|
||||
if (typeof next.content === "string") {
|
||||
next.content = processPII(next.content, true).text;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
return part;
|
||||
});
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const redactedMessages = messages.map((msg) => {
|
||||
if (typeof msg === "string") {
|
||||
return processPII(msg, true).text;
|
||||
}
|
||||
if (!msg || typeof msg !== "object") {
|
||||
return msg;
|
||||
}
|
||||
const next = { ...msg };
|
||||
if ("content" in next) {
|
||||
next.content = redactContentValue(next.content);
|
||||
}
|
||||
if (typeof next.text === "string") {
|
||||
next.text = processPII(next.text, true).text;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
|
||||
if (clone.messages !== undefined) {
|
||||
clone.messages = Array.isArray(clone.messages) ? redactedMessages : redactedMessages[0];
|
||||
} else if (clone.input !== undefined) {
|
||||
clone.input = Array.isArray(clone.input) ? redactedMessages : redactedMessages[0];
|
||||
}
|
||||
|
||||
if (typeof clone.system === "string") {
|
||||
clone.system = processPII(clone.system, true).text;
|
||||
} else if (Array.isArray(clone.system)) {
|
||||
clone.system = clone.system.map((entry) => {
|
||||
if (typeof entry === "string") return processPII(entry, true).text;
|
||||
if (entry && typeof entry === "object") {
|
||||
const next = { ...entry };
|
||||
if (typeof next.text === "string") next.text = processPII(next.text, true).text;
|
||||
if (typeof next.content === "string") next.content = processPII(next.content, true).text;
|
||||
return next;
|
||||
}
|
||||
return entry;
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof clone.input === "string") {
|
||||
clone.input = processPII(clone.input, true).text;
|
||||
}
|
||||
if (typeof clone.prompt === "string") {
|
||||
clone.prompt = processPII(clone.prompt, true).text;
|
||||
} else if (Array.isArray(clone.prompt)) {
|
||||
clone.prompt = clone.prompt.map((entry) =>
|
||||
typeof entry === "string" ? processPII(entry, true).text : entry
|
||||
);
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
// ─── Exports for Testing ──────────────────────────────────────────────
|
||||
|
||||
export { detectInjection, processPII, extractMessageContents, INJECTION_PATTERNS, PII_PATTERNS };
|
||||
|
||||
@@ -119,26 +119,49 @@ test("prompt injection guardrail blocks suspicious content in block mode", async
|
||||
);
|
||||
});
|
||||
|
||||
type ChatLikePayload = {
|
||||
messages?: Array<{ role?: string; content?: unknown }>;
|
||||
input?: unknown;
|
||||
choices?: Array<{ message?: { role?: string; content?: unknown } }>;
|
||||
};
|
||||
|
||||
test("pii masker guardrail redacts request and response payloads", async () => {
|
||||
// Request PII rewrite depends only on PII_REDACTION_ENABLED (not injection mode).
|
||||
await withEnv(
|
||||
{
|
||||
INPUT_SANITIZER_MODE: "redact",
|
||||
INPUT_SANITIZER_MODE: "block",
|
||||
PII_REDACTION_ENABLED: "true",
|
||||
PII_RESPONSE_SANITIZATION: "true",
|
||||
PII_RESPONSE_SANITIZATION_MODE: "redact",
|
||||
},
|
||||
async () => {
|
||||
const guardrail = new PIIMaskerGuardrail();
|
||||
|
||||
const preCall = await guardrail.preCall({
|
||||
messages: [{ role: "user", content: "Email me at dev@example.com" }],
|
||||
});
|
||||
assert.ok(preCall?.modifiedPayload);
|
||||
const preBody = preCall?.modifiedPayload as ChatLikePayload;
|
||||
assert.match(String(preBody.messages?.[0]?.content), /\[EMAIL_REDACTED\]/);
|
||||
|
||||
// Responses API can send plain string items in input[]
|
||||
const stringInput = await guardrail.preCall({
|
||||
input: ["Contact us at support@example.com for help"],
|
||||
});
|
||||
assert.ok(stringInput?.modifiedPayload);
|
||||
const stringBody = stringInput?.modifiedPayload as ChatLikePayload;
|
||||
assert.match(
|
||||
String((preCall?.modifiedPayload as Record<string, unknown>).messages?.[0]?.content),
|
||||
String(Array.isArray(stringBody.input) ? stringBody.input[0] : undefined),
|
||||
/\[EMAIL_REDACTED\]/
|
||||
);
|
||||
|
||||
// Top-level string input
|
||||
const topLevelInput = await guardrail.preCall({
|
||||
input: "Reach alice@example.com",
|
||||
});
|
||||
assert.ok(topLevelInput?.modifiedPayload);
|
||||
const topBody = topLevelInput?.modifiedPayload as ChatLikePayload;
|
||||
assert.match(String(topBody.input), /\[EMAIL_REDACTED\]/);
|
||||
|
||||
const postCall = await guardrail.postCall({
|
||||
choices: [
|
||||
{
|
||||
@@ -150,9 +173,8 @@ test("pii masker guardrail redacts request and response payloads", async () => {
|
||||
],
|
||||
});
|
||||
assert.ok(postCall?.modifiedResponse, "PII in response should trigger redaction");
|
||||
const redactedContent = String(
|
||||
(postCall?.modifiedResponse as Record<string, unknown>).choices?.[0]?.message?.content
|
||||
);
|
||||
const postBody = postCall?.modifiedResponse as ChatLikePayload;
|
||||
const redactedContent = String(postBody.choices?.[0]?.message?.content);
|
||||
assert.ok(
|
||||
redactedContent.includes("[EMAIL_REDACTED]") || redactedContent.includes("[PHONE_REDACTED]"),
|
||||
"email or phone should be redacted in response"
|
||||
@@ -161,6 +183,24 @@ test("pii masker guardrail redacts request and response payloads", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("pii masker does not rewrite request PII when redaction flag is off", async () => {
|
||||
await withEnv(
|
||||
{
|
||||
INPUT_SANITIZER_MODE: "redact",
|
||||
PII_REDACTION_ENABLED: "false",
|
||||
PII_RESPONSE_SANITIZATION: "false",
|
||||
},
|
||||
async () => {
|
||||
const guardrail = new PIIMaskerGuardrail();
|
||||
const preCall = await guardrail.preCall({
|
||||
messages: [{ role: "user", content: "Email me at dev@example.com" }],
|
||||
});
|
||||
assert.equal(preCall?.modifiedPayload, undefined);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
test("guardrail registry fails open when a guardrail throws", async () => {
|
||||
class ExplodingGuardrail extends BaseGuardrail {
|
||||
constructor() {
|
||||
|
||||
88
tests/unit/input-sanitizer-pii-redaction.test.ts
Normal file
88
tests/unit/input-sanitizer-pii-redaction.test.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { sanitizeRequest } from "../../src/shared/utils/inputSanitizer.ts";
|
||||
|
||||
async function withEnv(overrides: Record<string, string | undefined>, fn: () => Promise<void> | void) {
|
||||
const originals: Record<string, string | undefined> = {};
|
||||
for (const [key, value] of Object.entries(overrides)) {
|
||||
originals[key] = process.env[key];
|
||||
if (value === undefined) delete process.env[key];
|
||||
else process.env[key] = value;
|
||||
}
|
||||
try {
|
||||
await fn();
|
||||
} finally {
|
||||
for (const [key, value] of Object.entries(originals)) {
|
||||
if (value === undefined) delete process.env[key];
|
||||
else process.env[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const silentLogger = {
|
||||
info() {},
|
||||
warn() {},
|
||||
error() {},
|
||||
} as Pick<Console, "info" | "warn" | "error">;
|
||||
|
||||
type ChatBody = {
|
||||
messages?: Array<{ role?: string; content?: unknown }>;
|
||||
input?: unknown;
|
||||
};
|
||||
|
||||
test("sanitizeRequest redacts PII when enabled even if injection mode is block", async () => {
|
||||
await withEnv(
|
||||
{
|
||||
INPUT_SANITIZER_ENABLED: "true",
|
||||
INPUT_SANITIZER_MODE: "block",
|
||||
PII_REDACTION_ENABLED: "true",
|
||||
},
|
||||
() => {
|
||||
const result = sanitizeRequest(
|
||||
{ messages: [{ role: "user", content: "Email dev@example.com" }] },
|
||||
silentLogger
|
||||
);
|
||||
assert.equal(result.modified, true);
|
||||
assert.ok(result.sanitizedBody);
|
||||
const body = result.sanitizedBody as ChatBody;
|
||||
assert.match(String(body.messages?.[0]?.content), /\[EMAIL_REDACTED\]/);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("sanitizeRequest redacts Responses API string input items", async () => {
|
||||
await withEnv(
|
||||
{
|
||||
INPUT_SANITIZER_ENABLED: "true",
|
||||
INPUT_SANITIZER_MODE: "warn",
|
||||
PII_REDACTION_ENABLED: "true",
|
||||
},
|
||||
() => {
|
||||
const result = sanitizeRequest(
|
||||
{ input: ["Please write to support@example.com"] },
|
||||
silentLogger
|
||||
);
|
||||
assert.equal(result.modified, true);
|
||||
const body = result.sanitizedBody as ChatBody;
|
||||
assert.match(String(Array.isArray(body.input) ? body.input[0] : undefined), /\[EMAIL_REDACTED\]/);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("sanitizeRequest does not rewrite PII when PII_REDACTION_ENABLED is false", async () => {
|
||||
await withEnv(
|
||||
{
|
||||
INPUT_SANITIZER_ENABLED: "true",
|
||||
INPUT_SANITIZER_MODE: "redact",
|
||||
PII_REDACTION_ENABLED: "false",
|
||||
},
|
||||
() => {
|
||||
const result = sanitizeRequest(
|
||||
{ messages: [{ role: "user", content: "Email dev@example.com" }] },
|
||||
silentLogger
|
||||
);
|
||||
assert.equal(result.modified, false);
|
||||
assert.equal(result.sanitizedBody, null);
|
||||
}
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user