Security hardening, correctness fixes, and webhook support - #22
Merged
Conversation
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 `` 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
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/plainupload namedevil.htmlis stored as.txtand served asapplication/octet-streamwithattachmentdisposition, preventing stored XSS. Known-safe image types (PNG, JPEG, GIF, WebP) are still served inline with explicitContent-Type; SVG is served withattachmentto 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-localhostPUBLIC_BASE_URL, or explicitALLOWED_ORIGINS) ifSECRET_KEYorADMIN_PASSare 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_HOPSnow correctly reads from the right ofX-Forwarded-For(the entry appended by your own proxy), not the left (client-supplied, forgeable).Correctness Fixes
Page list pagination stability: Added
id DESCtiebreaker to theORDER BYclause so pages sharing the samesort_orderandupdated_atdon'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 missingbase_versionwith a 400 error (no silent fallback), matching the optimistic-lock contract ofPUT /api/pages/{slug}.Deactivated user lockout: Added
is_activecolumn 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_idownership.Comment ordering stability: Added
id ASCtiebreaker 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
WebhooksSectionadmin panel component and backend endpoints:GET/POST /api/webhooks— list and create webhooksPUT /api/webhooks/{id}— toggle active statusDELETE /api/webhooks/{id}— delete webhookWebhooks 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