Skip to content

Security hardening, correctness fixes, and webhook support - #22

Merged
PttCodingMan merged 2 commits into
mainfrom
claude/code-review-features-2ga6ht
Jul 13, 2026
Merged

Security hardening, correctness fixes, and webhook support#22
PttCodingMan merged 2 commits into
mainfrom
claude/code-review-features-2ga6ht

Conversation

@PttCodingMan

Copy link
Copy Markdown
Owner

This PR bundles three major improvement passes: security hardening, correctness fixes, and new webhook functionality.

Security Hardening

Media upload XSS prevention: File extensions are now derived from the validated MIME type, not the attacker-controlled filename. A text/plain upload named evil.html is stored as .txt and served as application/octet-stream with attachment disposition, preventing stored XSS. Known-safe image types (PNG, JPEG, GIF, WebP) are still served inline with explicit Content-Type; SVG is served with attachment to prevent top-level script execution.

Activity feed leak fix: Non-admin users can no longer enumerate group names, membership changes, or other users' API token names via /api/activity. Group/token activity is now admin-only; non-admins see only page activity for pages they can read.

Production deployment safety: The app now refuses to start on internet-facing deployments (detected via COOKIE_SECURE, non-localhost PUBLIC_BASE_URL, or explicit ALLOWED_ORIGINS) if SECRET_KEY or ADMIN_PASS are set to insecure defaults. Localhost dev still warns loudly but boots.

Login rate limiter memory bounds: The in-memory rate limiter now evicts oldest-touched IP entries once the dict exceeds 10,000 IPs, preventing memory leaks from IP-rotating attackers.

Proxy IP trust fix: TRUST_PROXY_HOPS now correctly reads from the right of X-Forwarded-For (the entry appended by your own proxy), not the left (client-supplied, forgeable).

Correctness Fixes

Page list pagination stability: Added id DESC tiebreaker to the ORDER BY clause so pages sharing the same sort_order and updated_at don't duplicate or skip across LIMIT/OFFSET boundaries.

Parent page validation: PUT /api/pages/{slug} now validates that a new parent exists and is not soft-deleted before applying the FK constraint, surfacing a clear 400 instead of an opaque 500.

Backlink survival across trash/restore: Backlinks are now preserved when the target page is trashed, the source is re-saved, and the target is restored. The wikilink parser deliberately does NOT skip soft-deleted targets during re-indexing.

Revert requires base_version: POST /api/pages/{slug}/revert/{version} now rejects requests missing base_version with a 400 error (no silent fallback), matching the optimistic-lock contract of PUT /api/pages/{slug}.

Deactivated user lockout: Added is_active column to users. A deactivated user (is_active=0) cannot log in and their existing JWT tokens stop resolving, enabling account suspension without data deletion.

Diagram ownership validation: Public pages can no longer exfiltrate diagrams owned by private pages; diagram inlining now checks page_id ownership.

Comment ordering stability: Added id ASC tiebreaker to comment ordering so same-second posts don't reorder across pagination.

Double-click guards: Watch and bookmark toggles now guard against in-flight requests to prevent desync with server state.

Comment edit error handling: Edit mode stays open if the update fails, preventing loss of unsaved edits.

Logout state cleanup: Logging out now resets per-user chat state so the next user signing in on the same tab doesn't see the previous user's conversation.

Non-401 error handling: The auth store no longer logs out on transient errors (500, network blips); only 401 triggers logout.

Webhook Support

Added a new WebhooksSection admin panel component and backend endpoints:

  • GET/POST /api/webhooks — list and create webhooks
  • PUT /api/webhooks/{id} — toggle active status
  • DELETE /api/webhooks/{id} — delete webhook

Webhooks POST a JSON payload to an external URL on page create/update/delete events. Supports event filtering (select which events trigger the hook) and enable/disable toggles. Includes i18n strings for English, Japanese, Korean

https://claude.ai/code/session_01WwrPN6d7VQBeWXtNpBnULy

claude added 2 commits July 13, 2026 09:17
Security:
- media: derive stored extension from validated MIME and serve non-image
files as neutral attachments (closes stored-XSS via .html/svg uploads)
- fail closed on insecure SECRET_KEY / weak ADMIN_PASS in production configs
- CSRF: exact-host localhost check (no http://localhost.evil.com bypass)
- activity feed: gate group/token activity to admins only
- OAuth: reject backslash open-redirect (/\evil.com)
- rate limit: read client IP right-to-left from X-Forwarded-For
(TRUST_PROXY_HOPS); bound login limiter memory
- public pages: only inline diagrams owned by public pages
Correctness:
- atomic optimistic lock on page update and version revert
(AND version = base_version + rowcount check)
- revert now requires base_version and fires watcher/@mention notifications
- keep backlinks to soft-deleted targets so they survive trash/edit/restore
- validate parent_id existence/non-deletion on update and move (400 not 500)
- FTS: mark page_id UNINDEXED (with migration) so numeric queries don't
match ids; rebuild helpers skip soft-deleted pages
- pagination tiebreaks; orphan-page stat ignores links from trashed pages
Frontend:
- editor keyed/gated on slug so a save can't write the previous page's body
- unsaved-changes guard covers in-app navigation and Cancel, not just unload
- reset chat history on logout; don't sign out on transient /auth/me errors
- surface comment post/edit/delete errors; guard watch/bookmark double-clicks
- localize callout titles via a shared, i18n-synced module
Features:
- is_active user deactivation wired through auth, update_user, and Admin UI
- Webhook management panel in Admin (backend API already existed)
Adds regression tests for media serve hardening, activity gating, backlink
restore, revert lock, and user deactivation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WwrPN6d7VQBeWXtNpBnULy
… render
The prior hardening downgraded all non-image media to octet-stream, which
broke legitimate `![icon](/api/media/x.svg)` embeds. SVG script only runs as
a top-level document, never via <img>, so serving image/svg+xml with an
attachment disposition + nosniff keeps inline rendering while still forcing a
download on direct navigation. Non-image, non-svg types stay octet-stream.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WwrPN6d7VQBeWXtNpBnULy
@PttCodingMan
PttCodingMan merged commit 4915bbe into mainJul 13, 2026
4 checks passed
Sign up for freeto 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.

2 participants

@PttCodingMan@claude