Skip to content

fix(server): security hardening — SECU audit batch 1#660

Merged
camathieu merged 16 commits into
masterfrom
secu/fixes
Mar 1, 2026
Merged

fix(server): security hardening — SECU audit batch 1#660
camathieu merged 16 commits into
masterfrom
secu/fixes

Conversation

@camathieu
Copy link
Copy Markdown
Member

@camathieu camathieu commented Mar 1, 2026

What

Batch of security fixes and hardening based on a security audit by @bewiwi. Covers input validation, authentication, content-type safety, transport security, and documentation.

Commits

  • 31f7971 fix(server): prevent non-admin MaxUserSize quota bypass
  • 4b03a1d fix(server): return 404 for cross-upload file access
  • 876850d fix(server): reject empty password when protection requested
  • 347de0b fix(server): handle logout gracefully when auth disabled
  • 37ea797 fix(server): neutralize SVG/XML/JS content types
  • 9207060 chore: gitignore security review documents
  • 063192d fix(server): limit upload comment length to 32KB
  • 586dd35 fix(webapp): add rel="noopener noreferrer" to download links
  • 8c48352 fix(server): enforce filename length limit in Content-Disposition
  • 79508e1 fix(server): strip Unicode BiDi overrides from Content-Disposition
  • 69d9578 fix(server): constant-time token comparison
  • d3b09f2 fix(server): disable directory listing on static handlers
  • 89ba3f3 fix(server): set Secure cookie flag when TLS enabled
  • e29dbc5 fix(server): add HSTS header when TLS enabled
  • e9e978d fix(server): use bcrypt(sha256) for upload password hashing
  • eee190c docs(security): document removable upload + password hashing

Changes

Authentication & Crypto

  • Upload passwords: MD5 → bcrypt(sha256(base64(login:password))) with 128-char limits
  • Legacy MD5 hashes accepted until uploads expire (fix-forward)
  • Constant-time comparison for upload tokens and API tokens
  • Secure cookie flag when SslEnabled || EnhancedWebSecurity
  • Reject empty password when protectedByPassword=true

Content Security

  • Neutralize SVG, XML, and JavaScript content types to application/octet-stream
  • Strip 9 Unicode BiDi override characters from Content-Disposition filenames
  • Enforce 1024-char filename length limit in Content-Disposition
  • Add rel="noopener noreferrer" to download links

Transport Security

  • HSTS header (max-age=31536000) when TLS is enabled

Input Validation

  • Limit upload comments to 32KB
  • Prevent non-admin MaxUserSize quota bypass in UpdateUser

Information Disclosure

  • Disable directory listing on /clients/, /changelog/, webapp root
  • Return 404 (not 500) for cross-upload file access

Documentation

  • Document password hashing scheme and limits
  • Document removable uploads behavior (by design: anyone can delete)

Testing

Each commit includes relevant unit tests. All make lint, make client server, make docs, make vuln pass.


Thanks to @bewiwi for the thorough security audit that identified these issues. 🙏

ca.mathieu added 16 commits March 1, 2026 14:55
The UpdateUser handler checked MaxTTL and MaxFileSize to prevent
non-admin users from modifying their own quotas, but did not check
MaxUserSize. A non-admin user could increase their own storage quota
by sending a modified MaxUserSize value in the update request.

Add MaxUserSize to the existing quota check and add a dedicated test.
When requesting a file that exists but belongs to a different upload,
the server returned HTTP 500 instead of 404. This allowed an attacker
to distinguish between non-existent file IDs (404) and file IDs that
exist in other uploads (500), enabling file ID enumeration.

Return 404 with the same message as a genuinely missing file.
When a client sent protectedByPassword=true with an empty password,
the server silently created an unprotected upload. This gave users a
false sense of security — they believed their upload was password-
protected when it was not.

Now return a 400 error with message 'upload password is empty' when
the client explicitly requests password protection but provides no
password.
The Logout handler called GetAuthenticator() which panics when the
authenticator is nil (authentication disabled). The panic recovery
middleware then returned HTTP 500.

Add GetAuthenticatorSafe() that returns nil instead of panicking,
and use it in the Logout handler to return 200 gracefully.
Extend content-type neutralization to cover SVG, XML, and JavaScript
in addition to the existing HTML, Flash, and PDF handling. All
dangerous types are now consistently forced to application/octet-stream
to prevent XSS via SVG onload handlers, XML rendering, or script
execution.

Document the neutralization rules in the security guide.
The comments field had no server-side length validation, allowing
clients to submit arbitrarily large comments. Add a 10,000 character
maximum and return an error when exceeded.
Anchor tags opening in a new tab without rel="noopener" allow the
opened page to access window.opener. Add the attribute to both the
filename link and the download button in FileRow.vue.
Truncate filenames to 1024 characters in SanitizeFilenameForDisposition
as defense-in-depth. The CreateFile path already validates length, but
this catches any code path that serves filenames in headers.
…lenames

Unicode bidirectional override characters (U+202A-U+202E, U+2066-U+2069)
can trick users about file extensions. For example, a filename containing
RLO (U+202E) can make 'evil<RLO>fdp.exe' appear as 'evil exe.pdf' in
some UIs. Strip all 9 BiDi formatting characters from filenames.
Replace plain == comparisons with crypto/subtle.ConstantTimeCompare
for upload token (X-UploadToken header) and API token verification.
Prevents theoretical timing attacks on token values.
Wrap /clients/, /changelog/, and webapp root FileServer handlers with
NoDirListing to return 404 for directory requests instead of generating
listings. Root '/' is allowed through for SPA index.html.
Session and XSRF cookies now get the Secure flag when either
EnhancedWebSecurity or SslEnabled is true, ensuring cookies are
never sent over plain HTTP when TLS is configured.
Add Strict-Transport-Security middleware (max-age=31536000, 1 year)
applied when SslEnabled or EnhancedWebSecurity is true. Tells browsers
to always use HTTPS for this domain. Updated security docs.
Replace MD5 with bcrypt(sha256(base64(login:password))) for upload
password storage. SHA-256 pre-hash removes bcrypt's 72-byte input limit.

- Add common.HashUploadPassword and common.CheckUploadPassword helpers
- Validate login and password length (max 128 characters each)
- Legacy MD5 hashes are still accepted for old uploads until they expire
- Add tests for bcrypt path, legacy MD5 path, and length validation
- Document password hashing scheme and limits in docs/guide/security.md
- Document SECU-20: removable uploads are deletable by anyone by design
- Document SECU-02: bcrypt(sha256) password hashing with 128-char limits
- Document legacy MD5 compatibility for uploads created before 1.4
@camathieu camathieu merged commit 2bab44c into master Mar 1, 2026
7 checks passed
@camathieu camathieu deleted the secu/fixes branch March 7, 2026 11:12
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.

1 participant