Skip to content

fix(security): allowlist library/draft writes + scope favorite deletes (P4.3) - #52

Merged
franciszver merged 2 commits into
mainfrom
fix/p4-backend-write-authz
Jul 24, 2026
Merged

fix(security): allowlist library/draft writes + scope favorite deletes (P4.3)#52
franciszver merged 2 commits into
mainfrom
fix/p4-backend-write-authz

Conversation

@franciszver

Copy link
Copy Markdown
Owner

Closes #49

Summary

Closes the mass-assignment / trust-flag-spoofing and favorite-IDOR findings from the Phase-4 review. Shared libraries stay user-editable (owner decision D18) — the fix restricts which fields clients may write, not who may write.

  • Write-field allowlists (pick() helper) on create+update for citation, clause, template repositories. Excludes trust/system fields: isVerified, isPublished, publishedAt, version, usageCount/lastUsedAt, createdBy, id, timestamps, relation refs. A normal user can no longer POST/PATCH {isVerified:true} a fabricated "verified" citation or publish arbitrary templates.
  • Draft update allowlist: title, content, metadata, intakeData, status only — userId/id can no longer be reassigned via PATCH /drafts/:id (document-planting closed).
  • Favorite delete IDOR: removeClauseFavorite/removeCitationFavorite now deleteMany({ where: { id, userId } }) with the authenticated req.user.id; a mismatched user deletes 0 rows.

No publish/verify/version endpoints exist in the server, so locking these fields breaks no legitimate flow (verified by cold review).

Red-first / tests

Red commit 8bfde53 (12 failing tests: trust-flag ignored on create+update per model, draft userId immutable, cross-user favorite IDOR — repo + route level) → green 311cfb5.

Verification

server/ npm test: 232 passed (220 baseline + 12).

Gates

/simplify + /security-review + /code-review via fresh cold reviewer, no implementation context — verdict clean, no functional regression, no remaining raw-body spread for these 4 models. Secret scan clean.

Follow-up noted (separate issue): auditLog/collaborator/shareLink repos still spread raw data, but those writes are field-guarded at the route layer.

Assisted-by: Claude Code (Fable orchestrator + Sonnet implementer/reviewer)

franciszver and others added 2 commits July 24, 2026 06:00
Adds failing tests against current repository/route behavior:
- updateCitation/updateClause/updateTemplate must not let clients set
  isVerified/isPublished (and createCitation/createClause/createTemplate
  must not let clients set them at creation either).
- updateDraft must not let clients reassign userId.
- removeClauseFavorite/removeCitationFavorite must not delete a favorite
  row owned by a different user (IDOR).

Also extends the fake Prisma test client with deleteMany, needed by the
upcoming userId-scoped favorite delete implementation.

Assisted-by: Claude Code (Sonnet)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s by userId (#49)

Clause/citation/template libraries stay user-editable by any authenticated
user (owner decision: no role gate), but clients could previously mass-
assign system/trust fields by spreading raw req.body into prisma create/
update calls:
- Citation.isVerified, Clause.isPublished, Template.isPublished/version
  could be set directly by any client on create or update.
- Draft.userId could be reassigned via PATCH /drafts/:id.
- removeClauseFavorite/removeCitationFavorite deleted by row id with no
  ownership check (IDOR: any authenticated user could delete any other
  user's favorite by guessing/enumerating ids).

Adds a small pick(obj, keys) helper (server/src/repositories/pick.js) and
per-model allowlists of client-writable content fields, built from
prisma/schema.prisma:
- Citation: title, citation, type, court, year, volume, reporter, page,
  pinpoint, jurisdiction, codeTitle, section, subdivision, shortForm,
  parenthetical, url, category, tags, notes.
- Clause: title, content, description, category, subcategory, tags,
  jurisdiction, documentTypes, variations, author, isFavorite, notes,
  placeholders.
- Template: category, name, skeletonContent, defaultMetadata,
  placeholders, sections, variables.
- Draft (update only): title, content, metadata, intakeData, status.

usageCount/lastUsedAt (managed by the dedicated /usage endpoints),
isVerified/isPublished/version/publishedAt/parentTemplateId (trust/version
fields), createdBy, and all ids/timestamps/relations are excluded from
client writes on all three library models.

removeClauseFavorite/removeCitationFavorite now take a userId and delete
via deleteMany({ where: { id, userId } }), so a mismatched user deletes
nothing; the DELETE /favorites/:id routes now pass req.user.id. Added
deleteMany to the fake Prisma test client to support this.

Adjusted templateRepository.test.js and clauseRepository.test.js fixtures
that previously set isPublished via createClause/updateTemplate directly
(now seeded via a raw prisma.*.update call instead, since that field is no
longer client-writable through the repository functions).

Assisted-by: Claude Code (Sonnet)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@franciszver
franciszver merged commit 2cb3a1c into main Jul 24, 2026
2 checks passed
@franciszver
franciszver deleted the fix/p4-backend-write-authz branch July 24, 2026 13:06
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.

P4.3: Allowlist writable fields on library & draft writes + scope favorite deletes by userId

1 participant