Skip to content

Commit d2f24d4

Browse files
committed
fix: deep-clone nested config before freezing to avoid side effects
getConfigOptions() was freezing nested plain objects in place via Object.freeze(val), which also froze the user's original objects since the spread copy is shallow. Now deep-clones with JSON.parse(JSON.stringify()) before freezing, so the returned config is immutable but the caller's originals remain mutable.
1 parent fcdd442 commit d2f24d4

2 files changed

Lines changed: 18 additions & 2 deletions

File tree

index.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,13 @@ module.exports = (options = {}) => {
6262

6363
getConfigOptions () {
6464
const copy = { ...options }
65-
// Deep-freeze nested plain objects (server is a live reference — exempted)
65+
// Deep-clone + freeze nested plain objects so the user's originals
66+
// are not mutated as a side effect of calling getConfigOptions().
6667
for (const key of Object.keys(copy)) {
6768
const val = copy[key]
6869
if (val && typeof val === 'object' && !Array.isArray(val) &&
6970
key !== 'server' && val.constructor === Object) {
70-
Object.freeze(val)
71+
copy[key] = Object.freeze(JSON.parse(JSON.stringify(val)))
7172
}
7273
}
7374
return Object.freeze(copy)

specs/security-review-2026.test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,21 @@ describe('Security Review — April 2026', () => {
408408
expect(Object.isFrozen(opts.customNested)).to.equal(true)
409409
})
410410

411+
it('should NOT freeze user-provided original nested objects (no side effects)', () => {
412+
const myConfig = { key: 'value', nested: { inner: 'secret' } }
413+
const service = require('../index')({
414+
customNested: myConfig
415+
})
416+
417+
// Read config — this should NOT freeze the original
418+
service.getConfigOptions()
419+
420+
// The user's original object must remain mutable
421+
expect(Object.isFrozen(myConfig)).to.equal(false)
422+
expect(() => { myConfig.key = 'updated' }).to.not.throw()
423+
expect(() => { myConfig.nested.inner = 'changed' }).to.not.throw()
424+
})
425+
411426
it('should prevent mutation of top-level config properties', () => {
412427
const service = require('../index')()
413428
const opts = service.getConfigOptions()

0 commit comments

Comments
 (0)