Skip to content

[2.4.6] - Defense & Speed

Choose a tag to compare

@CodeMeAPixel CodeMeAPixel released this 13 Jun 17:10
· 49 commits to master since this release
b502804

Security

  • ReDoS — Polynomial Regular Expression Hardening
    • Replaced all /\/+$/ trailing-slash regex patterns applied to user-controlled values with linear while-loop equivalents, eliminating O(n²) backtracking risk (CodeQL js/polynomial-redos).
    • Affected files: packages/lib/utils/index.ts (urlForHost), packages/lib/files/upload-validation.ts (domain cleaning), packages/lib/files/filename.ts (slug trimming), app/api/files/route.ts (URL construction).
    • Replaced /^-+|-+$/g alternation pattern in filename slug generation with pointer-based leading/trailing trim.
  • SSRF — Integration Test Endpoint Input Validation
    • Discord server ID (serverId) now validated against snowflake format (/^\d{17,20}$/) before being interpolated into the Discord API URL (CodeQL js/request-forgery).
    • Cloudflare account ID (accountId) now validated against 32-character lowercase hex format before URL construction.
    • Both integrations return a clear validation error message rather than making an outbound request with unsanitised input.
  • Miscellaneous CodeQL Findings Resolved
    • Incomplete URL substring sanitisation (alerts 6, 7) — hardened URL host checks.
    • Shell command built from environment values (alert 16) — environment input sanitised before shell interpolation.
    • Use of externally-controlled format string (alert 15) — format string construction tightened.
    • Additional SSRF alerts (10–13, 17–19) addressed across various API routes.

Changed

  • Status Page Integration Removed
    • Removed the Kener / Uptime Kuma dynamic status integration entirely. The polling logic, /api/status route (now returns 404), admin settings panel, and integration test handler have all been removed.
    • StatusIndicator in the site footer is now a lightweight static link to emberlystat.us — no external API calls, no runtime failures, no "Status unknown" states.
    • KENER_API_KEY, KENER_BASE_URL, UPTIME_KUMA_BASE_URL, and UPTIME_KUMA_SLUG environment variables are no longer used and can be removed.

Added

  • CodeQL Workflow — Automated static analysis via GitHub Actions (/.github/workflows/codeql.yml) now runs on push and pull request for continuous security scanning.
  • SECURITY.md — Added security policy documenting responsible disclosure process and supported versions.
  • License Scan — Added license scan report and status badge to repository.

Performance

  • VirusTotal scan moved off the critical path — VT hash lookups previously blocked the upload response for 5-10s on non-media files. The scan now runs in the background after the file is stored and the response is returned. Files detected as malicious are automatically quarantined (removed from storage, marked private) and logged.
  • Stripe subscription sync debounce survives hot-reloads — The per-user 5-minute Stripe sync cache (stripeSyncCache) was stored as a module-level variable, causing it to reset on every Next.js hot-reload in development and trigger a live Stripe API call on every upload. Moved to globalThis so the TTL is respected across reloads.
  • S3 provider singleton persisted across hot-reloads — Storage provider was re-initialized on every request in development for the same reason. Also moved to globalThis, eliminating redundant initialization logs and the associated config DB read per request.
  • File buffer, storage provider, and filename generation parallelizedarrayBuffer(), getStorageProvider(), and getUniqueFilename() now run concurrently with Promise.all instead of sequentially, removing 1-2 unnecessary round-trips from the critical path.
  • bcrypt.hash moved outside the DB transaction — Password hashing was running inside prisma.$transaction, blocking the database connection during a CPU-intensive operation. The hash is now computed before the transaction opens.

Fixed

  • Sitemap — Marked sitemap route as dynamic to prevent build-time errors when database is unavailable during static export.
  • TypeScript — Logger argument types in sync-buckets.tsBucketSyncStats was passed directly as a logger.info context argument (expects Record<string, unknown>); fixed by spreading with { ...stats }. Two logger.warn calls passed raw Error objects where a context object is expected; fixed by passing { error: String(err) }.
  • TypeScript — PaginationData.pages property in user-list.tsx — Three references used .pages on the PaginationData type returned by useUserManagement, which defines the property as pageCount. Corrected to .pageCount.
  • TypeScript — emailVerified type mismatch in app/api/files/route.ts — Prisma returns emailVerified: Date | null but AuthenticatedUser expects boolean. The squad-owner user object is now spread with emailVerified: ownerUser.emailVerified !== null before assignment.
  • ESLint — require() imports in migration scriptsscripts/migrate-config.js and scripts/hash-file-passwords.js used CommonJS require(), which is forbidden by the project's ESLint config. Both converted to ESM (import) and renamed to .mjs.
  • ESLint — Empty interfaces in react-jsx-compat.d.ts — Six interface X extends Y {} declarations with no added members replaced with type X = Y aliases, satisfying @typescript-eslint/no-empty-object-type.
  • ESLint — Unused variables across multiple files — Removed or prefixed unused imports and variables flagged by @typescript-eslint/no-unused-vars: Copy in bucket/page.tsx, User in blog/page.tsx, Share2/User in blog/[slug]/page.tsx, Footer/getConfig/providedPassword in [filename]/page.tsx, toast in squads/client.tsx, codeSent state in alpha-migration/page.tsx, and userName parameter in dashboard/client.tsx.
  • ESLint — letconst in theme-initializer.tsxcssVariables was declared with let but never reassigned; declaration moved to the single assignment site as const.
  • Integration test fetch timeoutstestStripe, testResend, testCloudflare, testDiscord (bot + webhook), and testGitHub had no request timeout, allowing the admin integration-test endpoint to hang indefinitely if a provider didn't respond. All now use AbortSignal.timeout(8000), consistent with the existing Vultr handler.
  • Quarantine failure loggingPromise.allSettled results in the VirusTotal quarantine path were silently discarded. Storage delete or DB update failures now log an error with the file ID so they can be investigated.
  • Session cache emailVerified backfill — Redis-cached sessions written before the emailVerified: boolean field was added would deserialize with undefined, breaking the upload auth contract. A coerce-on-read now sets false for any entry missing the field.
  • Legacy kener/uptimeKuma config keys stripped from admin response — The integration config schema uses .passthrough(), so old DB records containing stale Kener or Uptime Kuma keys would survive deepMerge and be returned to SUPERADMIN via GET /api/settings. maskSecretsForAdmin now explicitly deletes both keys before returning.
  • Empty filename slug fallback — Filenames composed entirely of non-ASCII/symbol characters would reduce to an empty slug after sanitization, producing broken storage paths. A nanoid(6) fallback is now used when the slug is empty.
  • storageQuotaMB = 0 admin override ignoredif (!baseQuotaMB) treated an explicit zero-quota override as "unset", falling through to plan-based quota. Changed to == null checks so 0 is honored as a deliberate override.
  • Storage-bucket subscription precedence missing from Stripe re-check path — After a successful Stripe sync, getPlanLimits returned the latest active subscription without first checking for a storage-bucket-* subscription. Users with both sub types active could receive non-unlimited limits for that request. The re-check now mirrors the original early-exit logic.
  • proxy.tsBASE_URL/MAIN_HOST recomputed per requestprocess.env.NEXT_PUBLIC_BASE_URL was parsed with new URL() on every invocation. Both the base URL string and its hostname are now computed once at module load time.
  • proxy.ts — Duplicate media-rewrite block eliminated — Video/audio range-request detection logic appeared twice (before and after the auth checks). Unified into a single block that runs before getToken(), so media requests skip JWT verification entirely.
  • proxy.tsVIDEO_EXTENSIONS array → SetVIDEO_EXTENSIONS.includes(ext) was an O(n) linear scan on every file URL request. Converted to a module-level Set for O(1) lookup.
  • proxy.ts — Trailing-slash strip regex replacedpathname.replace(/\/$/, '') replaced with endsWith('/')+slice, consistent with the ReDoS hardening applied elsewhere.
  • proxy.tsgetClientIP double-read of x-forwarded-for — The function read x-forwarded-for a second time at the fallback return if the header was already null at the top. Simplified to a single read with null-coalescing chain.
  • proxy.ts — Noisy console.log removed from hot paths — Debug logs for unverified-user blocks and password-breach redirects fired on every affected request, adding synchronous I/O overhead in the middleware layer.

What's Changed

New Contributors

Full Changelog: v2.4.5...v2.4.6