Skip to content

fix(security): keep the Redis credentials out of service serialization - #76

Merged
msalvatti merged 1 commit into
mainfrom
fix/connection-not-serializable
Aug 4, 2026
Merged

fix(security): keep the Redis credentials out of service serialization#76
msalvatti merged 1 commit into
mainfrom
fix/connection-not-serializable

Conversation

@msalvatti

Copy link
Copy Markdown
Member

What

Three distinct paths led from an injected service to the Redis password. Measured against the published 1.0.7 in a real NestJS consumer, walking QueueService:

QueueService.options.connection.url              → redis://default:<password>@…
QueueService.connection.options.connection.url   → redis://default:<password>@…
QueueService.connection.client.options.password  → <password>

JSON.stringify, a circular-safe stringifier (what pino and winston use) and util.inspect all emitted it in plaintext. That is what a structured logger does when it renders a provider it was handed, and what an error reporter does when it captures the scope of a throw.

The cause is uniform: TypeScript private is a compile-time annotation, erased at runtime. Every one of those fields was an ordinary enumerable own property.

The fix

Object Before After
resolved options connection plain field non-enumerable accessor
ConnectionResolver client + module options private #client, #options
QueueService.queues private #queues
QueueEventsRegistry.events / .connections private #events, #connections
WorkerRegistry.entries private #entries

The accessor rather than a non-enumerable value, because util.inspect({ showHidden: true }) still prints a hidden data property — and that is what a diagnostic dump uses. The maps are included because a BullMQ Queue, Worker or QueueEvents holds a live connection, so leaving them enumerable would re-open the path as soon as a queue is registered.

The consumer's own options object is never mutated; only objects this library constructs changed.

Compatibility

options.connection resolves exactly as before, and no public type or export moved. Ships as 1.0.8; README.md and CHANGELOG.md are in files, so the corrected security claim reaches npm with the code.

Verification

  • 279/279 tests, 100% line and branch, including new tests pinning containment on the resolved options and on ConnectionResolver (both assert showHidden), plus the reads-still-work cases.
  • typecheck, test:types, lint, build, size, check:published, smoke — all green.
  • The walker that found the three paths was re-run against the rebuilt tarball in the same consumer: enumerable paths reaching the secret: (none). ConnectionResolver now exposes only mode.

The resolved options carried `connection` as a plain field, ConnectionResolver
held the consumer's module options and the ioredis client in TypeScript
`private` properties, and the registries held their `Queue`, `Worker` and
`QueueEvents` maps the same way. `private` is erased at runtime, so every one of
those was an enumerable own property and the credentials were reachable by
walking a service: a connection `url` carries the password inline, and an
ioredis instance carries `options.password` as a plain field.

Verified against the published 1.0.7 in a real consumer, `JSON.stringify`,
a circular-safe stringifier and `util.inspect` on `QueueService` each emitted the
Redis password in plaintext through three distinct paths — the resolved options,
the resolver's copy of the module options, and the client itself. That is what a
structured logger does when it renders a provider it was handed, and what an
error reporter does when it captures the scope of a throw.

`connection` becomes a non-enumerable accessor on the resolved options; a
non-enumerable value would have sufficed for stringify and spread, but
`util.inspect({ showHidden: true })` still prints a hidden data property. The
resolver's client and options, and the three registry maps, become ECMAScript
private fields. Walking `QueueService` now reaches no credential by any path.

The consumer's own options object is never mutated — only the objects this
library constructs are changed. Reads are unchanged and no public type or export
moved.
Copilot AI lite review requested due to automatic review settings August 4, 2026 12:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the library against accidental Redis credential disclosure by making credential-bearing fields non-enumerable/non-serializable on injected services and resolved options, while preserving intentional access paths and public API behavior.

Changes:

  • Hide Redis credentials from incidental serialization by moving credential-bearing fields into ECMAScript #private fields and by attaching connection as a non-enumerable accessor on resolved options.
  • Update service registries (QueueService, WorkerRegistry, QueueEventsRegistry) to store BullMQ objects in #private maps to prevent serialization-based traversal to live connections.
  • Add tests that assert containment against JSON.stringify and util.inspect (including showHidden), and update docs/changelog with the revised security guarantee and release version bump.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/server/services/worker-registry.service.ts Stores worker registry map in an ECMAScript private field to prevent serialization walking into connections.
src/server/services/queue.service.ts Stores queue cache in an ECMAScript private field to prevent serialization walking into queue connections.
src/server/services/queue-events-registry.service.ts Stores QueueEvents + duplicated connection maps in ECMAScript private fields to prevent serialization walking into connections.
src/server/services/connection-resolver.service.ts Moves module options and resolved ioredis client into ECMAScript private fields to avoid credential exposure via service serialization.
src/server/services/connection-resolver.service.spec.ts Adds regression test asserting credentials are absent from Object.keys, JSON.stringify, and util.inspect(..., showHidden: true).
src/server/config/resolved-options.ts Attaches connection as a non-enumerable accessor on resolved options to block credential disclosure via common serialization paths.
src/server/config/resolved-options.spec.ts Adds regression tests ensuring incidental serialization paths don’t contain the Redis URL secret while property reads still work.
README.md Updates security documentation to reflect the new containment behavior across serialization paths.
package.json Bumps version to 1.0.8.
CHANGELOG.md Adds 1.0.8 entry describing the security fix and links the release comparison reference.

@msalvatti
msalvatti merged commit a1d7e8f into main Aug 4, 2026
18 checks passed
@msalvatti
msalvatti deleted the fix/connection-not-serializable branch August 4, 2026 13:12
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.

2 participants