Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8bcfee0
fix(logging): stop leaking env values and secrets in messages, make c…
kojiwakayama Aug 3, 2026
4955b4c
test(logging): assert complete sanitized API URL
kojiwakayama Aug 3, 2026
238b8aa
Preserve log fidelity while redacting credential-shaped values
kojiwakayama Aug 3, 2026
08d3bc5
fix(logging): harden redaction against intrinsic mutation
kojiwakayama Aug 3, 2026
628c04e
Share safe redacted serialization with telemetry
kojiwakayama Aug 3, 2026
502eb90
Harden logger serialization fallback
kojiwakayama Aug 3, 2026
12c084c
fix(logging): redact component names before fallback
kojiwakayama Aug 3, 2026
1277ee4
Avoid duplicate logging traversal on clean prototypes
kojiwakayama Aug 3, 2026
3a89347
fix(logging): capture collection redaction intrinsics
kojiwakayama Aug 3, 2026
6235461
fix(logging): retain intrinsic prototype identities
kojiwakayama Aug 3, 2026
44de703
fix(logging): detect intrinsic prototype-chain mutation
kojiwakayama Aug 3, 2026
abcd9d5
fix(logging): capture regexp exec during redaction
kojiwakayama Aug 3, 2026
d9558f0
fix(logging): avoid descriptor inheritance in regex redaction
kojiwakayama Aug 3, 2026
a5d7aef
fix(logging): bound hostile logger inputs
kojiwakayama Aug 3, 2026
470f985
Keep logger hook detection independent of Set
kojiwakayama Aug 3, 2026
3ddc182
fix(logging): close hostile-realm emission gaps
kojiwakayama Aug 3, 2026
8fe77a7
fix(logger): contain request context failures
kojiwakayama Aug 3, 2026
453e8d0
fix(logger): contain context timing and child calls
kojiwakayama Aug 3, 2026
39f3adc
fix(logger): guard composed logger outcomes
kojiwakayama Aug 3, 2026
7ef25ec
fix(logging): close remaining hostile emission and auth redaction gaps
kojiwakayama Aug 3, 2026
f4aafd0
Align emergency log component semantics
kojiwakayama Aug 3, 2026
43cf0aa
Keep hostile serialization tests inside the repository harness
kojiwakayama Aug 3, 2026
1cc5917
Reconcile logging hardening with the green release baseline
kojiwakayama Aug 3, 2026
d9a8253
Keep embedded RSC runtime aligned with hardened logging
kojiwakayama Aug 3, 2026
8e1a828
Merge current main into logging hardening
kojiwakayama Aug 3, 2026
417ec2f
Avoid duplicate component logger sanitization
kojiwakayama Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions src/observability/tracing/service-tracer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,92 @@ describe("observability/tracing/service-tracer", () => {
});
});

it("serializes object attributes without inherited toJSON hooks", () => {
const originalObjectToJSON = Object.getOwnPropertyDescriptor(Object.prototype, "toJSON");
const originalArrayToJSON = Object.getOwnPropertyDescriptor(Array.prototype, "toJSON");
Object.defineProperty(Object.prototype, "toJSON", {
configurable: true,
value() {
throw new Error("polluted object serializer");
},
});
Object.defineProperty(Array.prototype, "toJSON", {
configurable: true,
value() {
throw new Error("polluted array serializer");
},
});

try {
const harness = createHarness();
const serviceTracer = createOpenTelemetryServiceTracer({
serviceName: "test-service",
context: harness.contextApi,
trace: harness.traceApi,
errorStatusCode: 2,
});
const span = serviceTracer.tracer.startSpan("manual-operation");

span.setTag("metadata", {
apiKey: "secret",
nested: [{ ok: true }],
});

assertEquals(
harness.startedSpans[0]?.attributes.metadata,
'{"apiKey":"[REDACTED]","nested":[{"ok":true}]}',
);
} finally {
if (originalObjectToJSON) {
Object.defineProperty(Object.prototype, "toJSON", originalObjectToJSON);
} else {
delete (Object.prototype as { toJSON?: unknown }).toJSON;
}
if (originalArrayToJSON) {
Object.defineProperty(Array.prototype, "toJSON", originalArrayToJSON);
} else {
delete (Array.prototype as { toJSON?: unknown }).toJSON;
}
}
});

it("ignores hooks added through the intrinsic array prototype chain", () => {
const originalArrayPrototypeParent = Object.getPrototypeOf(Array.prototype);
let hookCalls = 0;
const hostileParent = Object.create(originalArrayPrototypeParent) as {
toJSON?: () => unknown;
};
hostileParent.toJSON = () => {
hookCalls += 1;
return "polluted-array";
};

Object.setPrototypeOf(Array.prototype, hostileParent);
try {
const harness = createHarness();
const serviceTracer = createOpenTelemetryServiceTracer({
serviceName: "test-service",
context: harness.contextApi,
trace: harness.traceApi,
errorStatusCode: 2,
});
const span = serviceTracer.tracer.startSpan("manual-operation");

span.setTag("metadata", {
apiKey: "secret",
nested: [{ ok: true }],
});

assertEquals(hookCalls, 0);
assertEquals(
harness.startedSpans[0]?.attributes.metadata,
'{"apiKey":"[REDACTED]","nested":[{"ok":true}]}',
);
} finally {
Object.setPrototypeOf(Array.prototype, originalArrayPrototypeParent);
}
});

it("isolates manual span attribute and finish failures", () => {
const harness = createHarness();
const serviceTracer = createOpenTelemetryServiceTracer({
Expand Down
10 changes: 2 additions & 8 deletions src/observability/tracing/service-tracer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { REDACTED, redactForSerialization } from "#veryfront/utils/logger/redact.ts";
import { stringifyRedactedAttributeValue } from "#veryfront/utils/logger/serialization.ts";
import { MAX_SPAN_NAME_LENGTH } from "#veryfront/utils/constants/index.ts";
import {
MAX_OBSERVABILITY_NAME_LENGTH,
Expand Down Expand Up @@ -158,13 +158,7 @@ function toAttributeValue(
}

if (typeof value === "object") {
try {
const redacted = redactForSerialization(value);
if (typeof redacted === "string") return redacted;
return JSON.stringify(redacted) ?? REDACTED;
} catch (_) {
return REDACTED;
}
return stringifyRedactedAttributeValue(value);
}

return value;
Expand Down
4 changes: 2 additions & 2 deletions src/server/services/rsc/endpoints/rsc-bundles.generated.ts

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions src/utils/env-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,5 +252,67 @@ describe("env-loader", () => {
__resetLoggerConfigForTests();
}
});

it("should not print environment values in debug logs", async () => {
const key = createKey("SECRET_LOG");
const secret = "highly-sensitive-value";
const previousLogLevel = getEnv("LOG_LEVEL");
const previousLogFormat = getEnv("LOG_FORMAT");
const originalDebug = console.debug;
const output: string[] = [];

try {
setEnv("LOG_LEVEL", "DEBUG");
setEnv("LOG_FORMAT", "json");
__resetLoggerConfigForTests();
console.debug = (message: string) => output.push(message);
await writeEnvFile(".env", `${key}=${secret}`);

await loadEnv({ cwd: tempDir, override: true, debug: true });

assertEquals(output.join("\n").includes("highly-sensitive"), false);
assertEquals(output.join("\n").includes(key), true);
} finally {
console.debug = originalDebug;
cleanupKeys(key);
if (previousLogLevel === undefined) deleteEnv("LOG_LEVEL");
else setEnv("LOG_LEVEL", previousLogLevel);
if (previousLogFormat === undefined) deleteEnv("LOG_FORMAT");
else setEnv("LOG_FORMAT", previousLogFormat);
__resetLoggerConfigForTests();
}
});

it("should strip credentials from the logged VERYFRONT_API_BASE_URL", async () => {
const previousValue = getEnv("VERYFRONT_API_BASE_URL");
const previousLogFormat = getEnv("LOG_FORMAT");
const { getOutput, restore } = captureConsoleLog();

try {
setEnv("LOG_FORMAT", "json");
__resetLoggerConfigForTests();
await writeEnvFile(
".env",
"VERYFRONT_API_BASE_URL=https://user:hybrid-basic-secret@api.example.com/api",
);

await loadEnv({ cwd: tempDir, override: true });

const output = getOutput();
const entry = JSON.parse(output) as LogEntry;
assertEquals(
entry.message,
"VERYFRONT_API_BASE_URL loaded: https://user:[REDACTED]@api.example.com/api",
);
assertEquals(output.includes("hybrid-basic-secret"), false);
} finally {
restore();
if (previousValue === undefined) deleteEnv("VERYFRONT_API_BASE_URL");
else setEnv("VERYFRONT_API_BASE_URL", previousValue);
if (previousLogFormat === undefined) deleteEnv("LOG_FORMAT");
else setEnv("LOG_FORMAT", previousLogFormat);
__resetLoggerConfigForTests();
}
});
});
});
11 changes: 7 additions & 4 deletions src/utils/env-loader.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { refreshLoggerConfig, serverLogger } from "./logger/index.ts";
import { sanitizeUrlCredentials } from "./logger/redact.ts";
import { cwd as getCwd, getEnv, setEnv } from "#veryfront/platform/compat/process.ts";
import { isNotFoundError, readTextFile } from "#veryfront/platform/compat/fs.ts";

Expand Down Expand Up @@ -37,13 +38,15 @@ export async function loadEnv(
envSources.set(key, file);
totalVars++;

// Log only the key name and value length — never any part of the value.
// Env files routinely carry credentials (VERYFRONT_API_TOKEN, DSNs), and
// a 20-char prefix is enough to leak most of a token.
if (debug) {
logger.debug(
`[env] ${key}=${value.substring(0, 20)}${value.length > 20 ? "..." : ""}`,
);
logger.debug(`[env] ${key} (${value.length} chars)`);
}
if (key === "VERYFRONT_API_BASE_URL") {
logger.info(`VERYFRONT_API_BASE_URL loaded: ${value}`);
// Hybrid setups can embed userinfo credentials in the URL; strip them.
logger.info(`VERYFRONT_API_BASE_URL loaded: ${sanitizeUrlCredentials(value)}`);
}
}

Expand Down
45 changes: 45 additions & 0 deletions src/utils/logger/logger-hostile-fallback.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const originalObjectValues = Object.values;
const originalObjectToJSON = Object.getOwnPropertyDescriptor(Object.prototype, "toJSON");
let output = "";

try {
Object.values = () => {
throw new Error("polluted Object.values");
};
Object.defineProperty(Object.prototype, "toJSON", {
configurable: true,
value() {
throw new Error("inherited serializer must not run");
},
});

// Initialize the serializer while the intrinsic is hostile so the logger's
// degraded fallback path is exercised after the global is restored.
await import("./serialization.ts");
Object.values = originalObjectValues;

Deno.env.set("LOG_FORMAT", "json");
const { __resetLoggerConfigForTests, getBaseLogger } = await import("./logger.ts");
__resetLoggerConfigForTests();

const originalConsoleLog = console.log;
try {
console.log = (value: unknown) => {
output = String(value);
};
getBaseLogger("SERVER")
.component("token=synthetic-component-secret")
.info("Fallback probe", { ok: true });
} finally {
console.log = originalConsoleLog;
}
} finally {
Object.values = originalObjectValues;
if (originalObjectToJSON !== undefined) {
Object.defineProperty(Object.prototype, "toJSON", originalObjectToJSON);
} else {
delete (Object.prototype as { toJSON?: unknown }).toJSON;
}
}

console.log(output);
Loading