Skip to content

fix: don't log raw Error objects in REST.ts (leaks secrets to hdb.log) (#1734) - #1737

Merged
kriszyp merged 3 commits into
mainfrom
kris/rest-log-error-leak-1734
Jul 10, 2026
Merged

fix: don't log raw Error objects in REST.ts (leaks secrets to hdb.log) (#1734)#1737
kriszyp merged 3 commits into
mainfrom
kris/rest-log-error-leak-1734

Conversation

@kriszyp

@kriszyp kriszyp commented Jul 9, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #1734. server/REST.ts's catch-all and WebSocket handlers passed the raw Error object to the logger. HarperLogger extends Console, so the Error is formatted with util.inspect, which dumps every own-enumerable property after the stack. Anything an app or an HTTP client library stashes on a thrown Error — a credential used for an outbound Authorization header, an axios config/request with headers — lands verbatim in hdb.log at the default log level, retrievable via the ops-API read_log operation.

Fix

Add harperLogger.errorForLog(error):

export function errorForLog(error: any) {
	return typeof error?.stack === 'string' ? error.stack : errorToString(error);
}

error.stack is the class name + message + frames — it does not contain custom own-enumerable properties — so logging the stack preserves debuggability while dropping the leak vector. Stackless/plain thrown values fall back to errorToString's "ClassName: message".

All six REST/WS log sites (HTTP catch-all warn/info/error, WS error handler, WS catch warn/info/error) now route through it. The client-response path (problemDetail) is untouched — it already minimizes to message/code/status/detail, so this is a server-log-only fix, distinct from #1421.

Design note

I log the stack rather than the issue's suggested error.message: stack is strictly safer than the current raw-object behavior yet keeps the frames operators rely on for 500s (message-only would regress debuggability). Message is still present — it's inside the stack — consistent with the already-accepted client-response title. This is a name-agnostic sanitizer, not an allowlist scrub, so it can't miss a "new sensitive property name."

Tests

4 new unit tests in harper_logger.test.js covering errorForLog, including an assertion that a Bearer token stashed on error.authorization/error.config does not appear in the output. tsc/oxlint/prettier clean.

🤖 Generated with Claude Code

#1734)

The REST catch-all and WS handlers passed the raw Error object to the
logger. HarperLogger extends Console, so the Error is formatted with
util.inspect, which dumps every own-enumerable property after the stack.
Any credential/config an app or HTTP client library stashes on a thrown
Error (e.g. an hdb_secret used for an outbound Authorization header, an
axios `config` with headers) lands verbatim in hdb.log at the default log
level, retrievable via read_log.

Add harperLogger.errorForLog(error), which logs the stack (class name +
message, no custom props) and falls back to "ClassName: message" for
stackless/plain thrown values. Route all six REST/WS log sites through it.
The client response path is unaffected — it already minimizes to
message/code/status/detail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the errorForLog utility function to safely format errors for logging, preventing sensitive stashed properties (such as credentials) from being leaked. The REST server has been updated to use this function across various error-handling paths, and corresponding unit tests have been added. The review feedback highlights a critical issue where passing null or undefined to errorForLog can trigger a TypeError crash in errorToString, and suggests adding defensive checks and additional test cases to handle these scenarios gracefully.

Comment thread utility/logging/harper_logger.ts
Comment thread unitTests/utility/logging/harper_logger.test.js
…iew)

errorForLog now returns a lazy util.inspect.custom wrapper instead of an
eagerly-built stack string. This:
- appends the `error.cause` chain (Harper leans on `new Error(msg, { cause })`;
  the old raw-object logging surfaced causes via util.inspect, plain `.stack`
  does not) — still excluding own-enumerable props, with cycle protection;
- only materializes the (potentially expensive) stack if the logger's level
  gate actually writes the entry, avoiding formatting on the discarded path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kriszyp

kriszyp commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

Cross-model review (standard: Gemini leg + Harper-domain adjudication)

No blockers. One significant concern addressed at the root, the rest triaged:

Addressed (2nd commit) — error.stack alone dropped the error.cause chain, which the old raw-object logging surfaced via util.inspect. Since Harper convention leans on new Error(msg, { cause }), that's a real diagnostic regression. errorForLog now returns a lazy util.inspect.custom wrapper that:

  • appends each cause's stack (cycle-protected), still excluding own-enumerable props; and
  • defers stack materialization until the logger's level gate actually writes — which also resolves Gemini's eager-.stack-on-the-discarded-path perf note in the same change.

Verified end-to-end through the real HarperLogger: a Bearer token on both error.authorization and the cause's config.headers.Authorization stays out of the rendered line, while message + stack + caused by: root-cause stack are all present. New unit tests cover the cause chain, the cycle guard, and the leak assertion (6 total).

Accepted / not changed:

  • Plain thrown object with no stack/message loses its fields (throw { status, details }errorToString"[object Object]"). Left as-is deliberately — rendering arbitrary object keys is the exact leak surface this fixes; Harper throws real Errors in practice. Minor.
  • errorForLog(null) would throw in the fallback — not reachable: every callsite dereferences error.statusCode/error.status first, so throw null already throws upstream of the logger.

Caveat: Codex leg didn't run (standard mode = Gemini leg only); domain pass + Gemini covered the logging/secret-leak surface.

Comment thread server/REST.ts
Comment thread utility/logging/harper_logger.ts Outdated
…g REST error logs (#1734)

Make errorToLogString null-safe so a thrown null/undefined value can't crash the
logger from inside a catch handler (errorForLog now renders it as "null"/"undefined"
instead of throwing on error.message).

Route the sibling HTTP request-error catch-alls through errorForLog so the #1734
secret-leak vector is closed consistently:
- server/http.ts Node onError handler
- server/http.ts Bun fetch handler catch
- server/graphqlQuerying.ts GraphQL HTTP catch-all

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kriszyp pushed a commit that referenced this pull request Jul 9, 2026
…sites

Several logger.error/warn/fatal call sites still passed raw Error objects
(or unhandledRejection/thrown-value reasons) directly to the logger, which
formats them with util.inspect and dumps every own-enumerable property —
leaking secrets that libraries stash on thrown Errors. Wrap each with the
existing errorForLog() lazy renderer (stack + cause chain only) instead.

Sites: componentLoader.ts, shutdownDrain.ts, scopeShutdown.ts,
components/mcp/resources.ts, fastifyRoutes.ts, operationsServer.ts,
workerProcessGuard.ts (unhandledRejection reason), JSONStream.ts,
jobRunner.ts, jobProcess.ts, and the two remaining http.ts sites not
already covered by #1737.

Files using `import * as harperLogger from harper_logger.ts` call
`harperLogger.errorForLog(...)` directly since the named export is already
reachable through the namespace import, rather than adding a redundant
named import alongside it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
import { PACKAGE_ROOT } from '../../utility/packageUtils.js';
import { _assignPackageExport } from '../../globals.js';
import { Console } from 'console';
import { inspect } from 'util';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scope module for consistency.

Suggested change
import { inspect } from 'util';
import { inspect } from 'node:util';

* what leaks secrets in #1734 (see `errorForLog`).
*/
function errorToLogString(error: any) {
let output = typeof error?.stack === 'string' ? error.stack : error == null ? String(error) : errorToString(error);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if the error == null check should be moved to errorToString()? Could that be beneficial elsewhere?

@kriszyp
kriszyp merged commit 9822f06 into main Jul 10, 2026
54 of 55 checks passed
@kriszyp
kriszyp deleted the kris/rest-log-error-leak-1734 branch July 10, 2026 02:55
kriszyp added a commit that referenced this pull request Jul 10, 2026
…st (#1734) (#1749)

* fix(logging): auto-wrap Error args in the logger + provenance-scoped console wraps (#1734)

Transplant of the kris/log-error-leak-sweep sweep onto main. #1737 (the
errorForLog primitive + REST.ts fix) was squash-merged, so the original
sweep branch's commit history no longer rebases cleanly; this reapplies its
net diff directly on top of the squashed main.

Enforces the invariant at the choke point instead of per call site: the
HarperLogger level methods (plus the inherited log/dir/table) route every
Error argument through errorForLog inside the level gate, so raw
logger.error(error) - anywhere, including future/component/app code holding
a server logger - cannot leak own-enumerable secrets into hdb.log. Explicit
errorForLog wraps remain only on console.* calls where the error's
provenance includes app/library code (JSONStream, socketRouter's
uncaughtException, componentLoader app-load failures, loadRootComponents'
package install) - console isn't auto-wrapped (rejected: it would replace
the inspector's reported call-site line with the wrapper's).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(logging): allowlist diagnostic error props + fold #1737 review comments (#1734)

Surface a small allowlist of own-enumerable Error properties in the rendered
log line - code, status, statusCode, errno, syscall - alongside the stack and
cause chain. These are common diagnostic fields (Node error codes, HTTP
status) that libraries/app code don't use to carry secrets, unlike arbitrary
properties (axios' config/request with an Authorization header), which stay
excluded. `path` is deliberately omitted - it can reveal internal filesystem
layout and the message/stack already names the failing operation.

Also folds in two review comments from #1737 that carried over to this
branch (both left unresolved when that PR was squash-merged):
- scope the util import to node:util for consistency
- move errorToString's null-guard into errorToString itself rather than
  duplicating it in errorToLogString, since errorToString is called directly
  at several response-body call sites (REST.ts, http.ts, JSONStream,
  ResourceBridge) that get the same defensive benefit for free.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(logging): harden the cause-chain render path against hostile objects (#1734)

gemini-code-assist review on #1749: isErrorLike's try/catch only protects the
top-level sanitizeErrorArgs decision. Once an error is wrapped, rendering
walks its cause chain unconditionally - a revoked Proxy or a throwing getter
anywhere in that chain (attached by code this module doesn't control) throws
inside errorToLogString, crashing the log write itself. Reproduced:
`error?.stack` on a revoked Proxy throws even through optional chaining
(?. only guards null/undefined, not trap exceptions).

Wraps every property access in the render path (errorToString,
loggablePropsSuffix, renderErrorLine, the cause-chain walk) in try/catch with
safe fallbacks, so the logger genuinely cannot throw regardless of what a
thrown Error's cause chain contains. errorToString is hardened at its own
level (not just via callers) since it also has external callers building
response bodies (REST.ts/http.ts/JSONStream).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Kris Zyp <kris@harperdb.io>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

server/REST.ts logs the raw Error object to hdb.log — leaks secrets/sensitive data attached to escaping Errors

2 participants