fix: don't log raw Error objects in REST.ts (leaks secrets to hdb.log) (#1734) - #1737
Conversation
#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>
|
Reviewed; no blockers found. |
There was a problem hiding this comment.
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.
…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>
Cross-model review (standard: Gemini leg + Harper-domain adjudication)No blockers. One significant concern addressed at the root, the rest triaged: Addressed (2nd commit) —
Verified end-to-end through the real Accepted / not changed:
Caveat: Codex leg didn't run (standard mode = Gemini leg only); domain pass + Gemini covered the logging/secret-leak surface. |
…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>
…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'; |
There was a problem hiding this comment.
Scope module for consistency.
| 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); |
There was a problem hiding this comment.
I wonder if the error == null check should be moved to errorToString()? Could that be beneficial elsewhere?
…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>
Summary
Fixes #1734.
server/REST.ts's catch-all and WebSocket handlers passed the rawErrorobject to the logger.HarperLogger extends Console, so the Error is formatted withutil.inspect, which dumps every own-enumerable property after the stack. Anything an app or an HTTP client library stashes on a thrownError— a credential used for an outboundAuthorizationheader, an axiosconfig/requestwith headers — lands verbatim inhdb.logat the default log level, retrievable via the ops-APIread_logoperation.Fix
Add
harperLogger.errorForLog(error):error.stackis 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 toerrorToString's"ClassName: message".All six REST/WS log sites (HTTP catch-all
warn/info/error, WSerrorhandler, WS catchwarn/info/error) now route through it. The client-response path (problemDetail) is untouched — it already minimizes tomessage/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-responsetitle. 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.jscoveringerrorForLog, including an assertion that aBearertoken stashed onerror.authorization/error.configdoes not appear in the output.tsc/oxlint/prettierclean.🤖 Generated with Claude Code