Skip to content

Enhance static file serving with security & caching improvements#65

Merged
MarcosBrendonDePaula merged 3 commits intomainfrom
claude/enhance-static-files-plugin-t1ibC
Feb 21, 2026
Merged

Enhance static file serving with security & caching improvements#65
MarcosBrendonDePaula merged 3 commits intomainfrom
claude/enhance-static-files-plugin-t1ibC

Conversation

@MarcosBrendonDePaula
Copy link
Copy Markdown
Collaborator

Summary

This PR significantly improves the static file serving infrastructure with comprehensive security hardening, intelligent caching strategies, and better HTTP semantics. Changes span both the core static files plugin and the Vite integration.

Key Changes

Static Files Plugin (core/server/plugins/static-files-plugin.ts)

  • Security hardening: Added detection and forced-download enforcement for dangerous MIME types and executable file extensions (.exe, .dll, .php, .svg, etc.)
  • Filename sanitization: Implemented sanitizeFilename() to prevent directory traversal and control character injection in Content-Disposition headers
  • Null byte rejection: Early validation to prevent filesystem confusion attacks
  • HTTP caching improvements:
    • Implemented weak ETag generation based on file size and modification time
    • Added conditional request handling (If-None-Match, If-Modified-Since) with 304 responses
    • Differentiated cache strategies: immutable assets (hashed filenames) get long-lived cache, uploads get short-lived with revalidation
  • Configuration-driven behavior: Made public/uploads routes optional via pluginsConfig flags
  • Security headers: Added X-Content-Type-Options: nosniff to prevent MIME type sniffing
  • Reduced syscalls: Combined existence check and file type validation into single statSync() call

Vite Integration (core/plugins/built-in/vite/index.ts)

  • Directory validation: collectFiles() now throws with a clear error message if the build output directory doesn't exist, preventing silent failures
  • Pre-compressed asset support: Detects .gz variants and serves them when client supports gzip encoding, with proper Content-Encoding and Content-Type headers
  • SPA fallback caching: Changed index.html fallback to use Cache-Control: no-cache instead of default caching, ensuring browsers check for updates on deploy
  • Response streaming: Changed Vite proxy to stream response bodies instead of buffering entire payloads in memory, improving performance for large assets

Implementation Details

  • Dangerous file extensions and MIME types are defined as immutable Set collections for O(1) lookup
  • Hashed asset detection uses regex pattern /\.[0-9a-f]{8,}\.\w+$/ to identify immutable build artifacts
  • Conditional request handling properly prioritizes ETag over Last-Modified per HTTP spec
  • Gzip variant detection pre-scans the file map at startup for efficient runtime lookups

https://claude.ai/code/session_01R51VjyiwsBiJic3npffRoc

…eaming, and config support

Static-files plugin:
- Use pluginsConfig values (staticPublicDir, staticUploadsDir, staticCacheMaxAge)
  instead of hardcoded paths
- Respect enablePublic / enableUploads config flags to selectively register routes
- Add ETag headers with 304 Not Modified support
- Add X-Content-Type-Options: nosniff on all responses
- Differentiate cache strategy: immutable for hashed assets, short TTL
  with must-revalidate for uploads
- Force Content-Disposition: attachment for dangerous MIME types/extensions

Vite plugin:
- Serve pre-compressed .gz files when Accept-Encoding includes gzip
- Fix SPA fallback to use Cache-Control: no-cache so new deploys are
  picked up immediately
- Stream proxy responses (response.body) instead of buffering with
  arrayBuffer()
- Fix collectFiles to throw explicitly when the build directory is
  missing instead of silently returning an empty map

https://claude.ai/code/session_01R51VjyiwsBiJic3npffRoc
…ments

- Eliminate double syscall: remove existsSync() before statSync(), use
  a single statSync() in try-catch
- Block null byte injection: reject paths containing \0 early
- Add Last-Modified header alongside ETag for full conditional request
  support (If-Modified-Since)
- Use weak ETag (W/"...") since it is derived from stat metadata, not
  content hash
- Return null instead of empty string for 304 responses to avoid
  unnecessary Content-Length: 0
- Sanitize filenames in Content-Disposition header to prevent header
  injection from special characters
- Use imported Stats type consistently instead of ReturnType<typeof statSync>
- Use path.basename() instead of manual split('/').pop()
- Remove hardcoded uploads/avatars directory creation (application concern)

https://claude.ai/code/session_01R51VjyiwsBiJic3npffRoc
static-files plugin:
- Replace fs.statSync with Bun.file().stat() (async, non-blocking I/O)
- Use file.lastModified (Bun-native property) for ETag and Last-Modified
  instead of importing Stats from fs
- Remove fs.statSync and fs.existsSync imports entirely — zero Node fs
  dependency
- Handler is now async, enabling non-blocking file metadata lookups
- Bun.file() return still uses sendfile(2) for zero-copy kernel transfer

vite plugin:
- Replace recursive readdirSync with Bun.Glob("**/*").scanSync() —
  native C++ glob implementation, no manual recursion
- Use Bun.Glob from global (not import from "bun") to stay compatible
  with Vitest module resolution
- Simplify collectFiles from 25 lines of recursive logic to a flat loop
- Remove readdirSync and statSync imports

https://claude.ai/code/session_01R51VjyiwsBiJic3npffRoc
@MarcosBrendonDePaula MarcosBrendonDePaula merged commit fd53b4a into main Feb 21, 2026
11 checks passed
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