-
-
Notifications
You must be signed in to change notification settings - Fork 7.3k
Description
POST /:name currently accepts client-supplied id values and does not enforce uniqueness.
That allows duplicate IDs to be inserted into a collection. After that, item-level operations such as GET /:name/:id and PATCH /:name/:id behave as first-match operations rather than unique-record operations, which creates ambiguous and potentially destructive behavior.
I checked the open tracker and did not find an existing issue that describes this exact behavior. The closest thing I found was #279, but that is about custom identifier field names, not duplicate-ID insertion.
Affected area
src/service.tssrc/random-id.ts
Reproduction
Run this from the repository root:
node --input-type=module -e "import { createApp } from './src/app.ts'; import { Low, Memory } from 'lowdb'; const db=new Low(new Memory(), {}); db.data={posts:[{id:'victim',title:'original'}]}; const app=createApp(db); const server=app.listen(4581, async ()=>{ const create=await fetch('http://127.0.0.1:4581/posts',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({id:'victim',title:'attacker'})}); const before=await fetch('http://127.0.0.1:4581/posts/victim'); const patch=await fetch('http://127.0.0.1:4581/posts/victim',{method:'PATCH',headers:{'content-type':'application/json'},body:JSON.stringify({title:'patched-first-match'})}); const all=await fetch('http://127.0.0.1:4581/posts'); console.log('create', await create.text()); console.log('before', await before.text()); console.log('patch', await patch.text()); console.log('all', await all.text()); server.close(); });"Expected behavior
- A duplicate
idinsert should be rejected, or - The system should preserve uniqueness so later item-level operations remain unambiguous.
Actual behavior
create {
"id": "victim",
"title": "attacker"
}
before {
"id": "victim",
"title": "original"
}
patch {
"id": "victim",
"title": "patched-first-match"
}
all [
{
"id": "victim",
"title": "patched-first-match"
},
{
"id": "victim",
"title": "attacker"
}
]
Root cause
The create path constructs records like this:
const item = { id: randomId(), ...data }Because ...data comes last, a client-supplied id overrides the generated one. There also does not appear to be a uniqueness check before insertion.
Additional note on generated IDs
The generated IDs currently come from randomBytes(2).toString('hex'), which means the namespace is only 65,536 values. That does not cause the duplicate-ID shadowing by itself, but it does make collisions realistic under enough inserts.
Impact
This is primarily a data-integrity issue:
- item-level reads become ambiguous,
- writes target only the first duplicate,
- deletes can remove one record while leaving another with the same ID behind.
Suggested fix
- Reject client-supplied
idby default, or gate it behind an explicit option. - Enforce uniqueness on insert.
- Consider increasing generated ID entropy if these IDs are meant to behave as stable identifiers.