Skip to content

POST accepts duplicate ids, causing ambiguous GET/PATCH/DELETE behavior #1732

@x4cc3

Description

@x4cc3

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.ts
  • src/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 id insert 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 id by 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions