Skip to content

security(sections): fix IDOR — private templates leaked/writable by slug#75

Open
Chzhqv wants to merge 1 commit into
TemaDeveloper:mainfrom
Chzhqv:fix/section-template-idor
Open

security(sections): fix IDOR — private templates leaked/writable by slug#75
Chzhqv wants to merge 1 commit into
TemaDeveloper:mainfrom
Chzhqv:fix/section-template-idor

Conversation

@Chzhqv

@Chzhqv Chzhqv commented Jul 7, 2026

Copy link
Copy Markdown

Summary

Every custom/AI-generated SectionTemplate defaulted to isShared: true (schema default), and neither buildTemplateDoc (profile-generated sections) nor the manual templates POST overrode it — so every personal section a user creates, including ones whose name/description describe their actual life, was silently public.

On top of that, several per-slug endpoints applied no ownership or usageCount gate at all, unlike the list endpoint (which requires isShared + usageCount >= 3):

  • GET /api/sections/templates/[slug] — had isShared:true with no usageCount check
  • GET/POST /api/sections/[slug]/entries — no gate whatsoever
  • PATCH /api/sections/[slug]/entries/[id] — no gate whatsoever
  • GET /api/export/[section] (custom section export) — no gate, leaked field labels via exported column headers
  • POST /api/sections/templates/[slug]/edit-layout — no gate, and worse: the save:true path let any user overwrite another user's template's layoutHtml by slug (write IDOR, not just a read leak)

Fixes

  • Schema default isShared: false (private unless explicitly shared)
  • buildTemplateDoc and the manual template POST now set isShared: false explicitly
  • All five endpoints above now apply the same ownership/shared gate used by the list endpoint: createdBy === user, or createdBy null (built-in), or isShared + usageCount >= 3 (proven shared template)
  • edit-layout's save path additionally requires strict ownership (createdBy === userId) even for templates matched via the shared/built-in branches of that gate, since read-for-AI-context and write-back are different trust levels
  • Updated a pre-existing unit test that had encoded the old insecure default (isShared: true) as expected schema behavior

Test plan

Verified live with two real registered users — created a private section as User A, then as User B attempted:

  1. Reading A's template definition — blocked (404)
  2. Reading A's entries — blocked (404)
  3. Posting entries against A's template — blocked (404)
  4. Exporting A's section (field-label leak check) — blocked (400 "Unknown section")
  5. PATCHing A's entry via the slug route — blocked (404)
  6. Overwriting A's layoutHtml via edit-layout save:trueblocked (404), confirmed layoutHtml unchanged in the DB

Confirmed no regression:

  • A's own access to all of the above still works (export produces a real .xlsx, entries list correctly, etc.)
  • A genuinely proven shared template (isShared:true, usageCount>=3, different owner) is still correctly readable by another user
  • An unproven shared template (usageCount<3) is correctly still blocked for a non-owner
  • pnpm lint, tsc --noEmit, full vitest run (556/556) all pass

Fixes #44

Every custom/AI-generated SectionTemplate defaulted to isShared:true
(schema default), and neither buildTemplateDoc (profile-generated
sections) nor the manual templates POST overrode it — so every
personal section a user creates (including ones whose name/description
describe their actual life) was silently public.

On top of that, several per-slug endpoints applied no ownership or
usageCount gate at all, unlike the list endpoint (which requires
isShared + usageCount >= 3):
- GET /api/sections/templates/[slug] — had isShared:true with no
  usageCount check, letting a template become public the instant a
  single other user's search happened to hit it
- GET/POST /api/sections/[slug]/entries — no gate whatsoever
- GET /api/sections/[slug]/entries/[id] (PATCH) — no gate whatsoever
- GET /api/export/[section] (custom: section export) — no gate,
  leaked field labels via exported column headers
- POST /api/sections/templates/[slug]/edit-layout — no gate, and
  worse: the save:true path let ANY user overwrite another user's
  template's layoutHtml by slug (write IDOR, not just a read leak)

Fixes:
- Schema default isShared: false (private unless explicitly shared)
- buildTemplateDoc and the manual template POST now set isShared:
  false explicitly
- All five endpoints above now apply the same ownership/shared gate
  used by the list endpoint: createdBy === user, or createdBy null
  (built-in), or isShared + usageCount >= 3 (proven shared template)
- edit-layout's save path additionally requires strict ownership
  (createdBy === userId) even for templates matched via the shared/
  built-in branches of that gate, since read-for-AI-context and
  write-back are different trust levels

Updated a pre-existing unit test that had encoded the old insecure
default (isShared: true) as expected schema behavior.

Verified live with two real registered users: created a private
section as User A, then as User B attempted (1) reading A's template
definition, (2) reading/(3) posting A's entries, (4) exporting A's
section, (5) PATCHing A's entry via the slug route, (6) overwriting
A's layoutHtml via edit-layout save:true — all six blocked with 404
(layoutHtml confirmed unchanged in the DB for TemaDeveloper#6). Confirmed no
regression: A's own access to all of the above still works, and a
genuinely proven shared template (isShared:true, usageCount>=3,
different owner) is still correctly readable by other users, while
an unproven one (usageCount<3) is correctly still blocked.

Fixes TemaDeveloper#44
@vercel

vercel Bot commented Jul 7, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the Artemii's projects Team on Vercel.

A member of the Team first needs to authorize it.

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.

security: Personal AI-generated section templates default to isShared:true — leak by slug (IDOR)

1 participant