Skip to content

Add security headers and file access restrictions - #32

Open
codeling wants to merge 3 commits into
mainfrom
claude/validate-page-names-and-upload-types
Open

codeling wants to merge 3 commits into
mainfrom
claude/validate-page-names-and-upload-types

Conversation

@codeling

@codeling codeling commented Oct 4, 2026

Copy link
Copy Markdown
Owner

Summary

This change implements comprehensive security hardening by adding security headers to all responses and restricting direct access to non-public files through web server configuration and .htaccess rules.

Key Changes

Security Headers

  • Added securityHeaders() function in auth_functions.php that sends security headers with every response:
    • Content-Security-Policy: Restricts scripts/styles to same-origin only, with nonce-based inline script support and hash-based inline event handler support
    • X-Content-Type-Options: nosniff
    • X-Frame-Options: DENY
    • Referrer-Policy: same-origin
    • Permissions-Policy: Disables camera, microphone, geolocation, payment, and USB
    • Cross-Origin-Opener-Policy: same-origin
    • Strict-Transport-Security (HTTPS only)
  • Added cspNonce() function to generate a unique nonce for each request's inline scripts
  • Defined constants for inline event handlers (HANDLER_TOGGLE_DRAWER, HANDLER_GO_BACK) to allow them via CSP hash validation
  • Updated index.php to use nonce for the upload form's inline script and constants for event handlers

File Access Restrictions

  • Added .htaccess rules to deny direct access to:
    • Non-entry-point PHP files (only index.php and api.php are public)
    • Configuration and dependency files (composer.json, phpunit.xml, documentation, license)
  • Added .htaccess files in tests/, locales/, and Michelf/ directories to deny direct access
  • Updated nginx configuration template and INSTALL.md with equivalent location rules
  • Added X-Content-Type-Options: nosniff header to static assets (JS, CSS, SVG, PNG)

Upload Security

  • Added uploadTypesForExt() and uploadTypeMatchesExt() functions to validate that uploaded file content matches the declared extension (prevents storing PDFs as PNGs, etc.)
  • Added imageMagickFormatPrefix() to explicitly specify format to ImageMagick instead of guessing from content
  • Added MAX_IMAGE_PIXELS configuration constant to refuse images exceeding pixel limits
  • Updated upload handling to check pixel count before processing and validate content type matches extension

Bug Fixes

  • Fixed page name validation logic to check invalid names even when isNew flag is false (client-controlled)
  • Fixed error message logic to correctly distinguish between "already exists" and "invalid name" errors
  • Fixed action state to remain 'edit' when validation fails on existing pages

Testing

  • Added SecurityHeadersTest integration test to verify all security headers are present on various endpoints
  • Added ImageTooLargeTest to verify pixel limit enforcement
  • Added ImageProcessingTest::testContentOfAnotherAcceptedTypeIsNotStoredUnderAnImageExtension() to verify type/extension validation
  • Added PageNameTest tests for invalid names on non-new pages and editing existing pages
  • Added ServerConfigTest tests to verify unneeded files are not served and entry points work correctly
  • Added IpAllowlistDeniedTest::testRefusedResponsesHaveSecurityHeaders() to verify headers on denied requests
  • Added unit tests for securityHeaders() and contentSecurityPolicy() functions

https://claude.ai/code/session_01HYvivwGowMczvsbn1ciBmR

claude added 3 commits October 4, 2026 06:24
…extension

- Saving an existing page (isNew unset) skipped isValidPageName(), so pages
  could be created in hidden folders or in the statically served uploads
  folder. The name is now checked for every save.
- Uploads only checked that the detected content type and the extension were
  each allowed, so e.g. a PDF could be stored as x.png and then be handed to
  ImageMagick as PDF. The content type must now match the extension, the
  format is passed to ImageMagick explicitly, and images above the new
  MAX_IMAGE_PIXELS setting are refused before being decoded.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HYvivwGowMczvsbn1ciBmR
- index.php and api.php now send a Content-Security-Policy (own scripts and
  styles only, nonce for the upload page script, hashes for the wiki's two
  inline handlers, no framing/plugins/base, forms to self), nosniff,
  X-Frame-Options, Referrer-Policy, Permissions-Policy, COOP and, over HTTPS,
  Strict-Transport-Security.
- Apache: only index.php and api.php are reachable among the PHP scripts;
  composer/phpunit files, markdown docs and the license are denied; the tests,
  locales and Michelf folders get their own .htaccess denying access.
- nginx: equivalent rules in INSTALL.md and the tested template; static files
  get nosniff.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HYvivwGowMczvsbn1ciBmR
The control file was placed in Michelf/, which now denies all access, so the
scripts in it could no longer run and the test could not notice them.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HYvivwGowMczvsbn1ciBmR
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.

2 participants