fix(security): keep the Redis credentials out of service serialization - #76
Merged
Conversation
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.
There was a problem hiding this comment.
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
#privatefields and by attachingconnectionas a non-enumerable accessor on resolved options. - Update service registries (
QueueService,WorkerRegistry,QueueEventsRegistry) to store BullMQ objects in#privatemaps to prevent serialization-based traversal to live connections. - Add tests that assert containment against
JSON.stringifyandutil.inspect(includingshowHidden), 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:JSON.stringify, a circular-safe stringifier (what pino and winston use) andutil.inspectall 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
privateis a compile-time annotation, erased at runtime. Every one of those fields was an ordinary enumerable own property.The fix
connectionConnectionResolverclient + module optionsprivate#client,#optionsQueueService.queuesprivate#queuesQueueEventsRegistry.events/.connectionsprivate#events,#connectionsWorkerRegistry.entriesprivate#entriesThe 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 BullMQQueue,WorkerorQueueEventsholds 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.connectionresolves exactly as before, and no public type or export moved. Ships as 1.0.8;README.mdandCHANGELOG.mdare infiles, so the corrected security claim reaches npm with the code.Verification
ConnectionResolver(both assertshowHidden), plus the reads-still-work cases.typecheck,test:types,lint,build,size,check:published,smoke— all green.enumerable paths reaching the secret: (none).ConnectionResolvernow exposes onlymode.