[api] audit fixes: account tests, dependencies and security hardening (#54)

* test(accounts): cover three username changes

* chore(deps): update vulnerable runtime dependencies

* fix(security): bound uploads and validate token subjects strictly

---------

Co-authored-by: Nexi (CWN) <communityshieldofficial@gmail.com>
This commit is contained in:
Nexi
2026-09-09 04:47:58 +01:00
committed by GitHub
parent 9696c56317
commit 222547ee13
38 changed files with 1161 additions and 632 deletions
+2 -2
View File
@@ -19,7 +19,7 @@
"@repo/jwt": "workspace:*",
"@standard-community/standard-json": "0.3.5",
"@standard-community/standard-openapi": "0.2.9",
"hono": "4.12.27",
"hono": "4.13.5",
"hono-openapi": "1.3.1",
"openapi-types": "12.1.3",
"workers-tagged-logger": "1.0.1",
@@ -31,6 +31,6 @@
"@repo/typescript-config": "workspace:*",
"@types/node": "26.0.1",
"vitest": "4.1.9",
"wrangler": "4.105.0"
"wrangler": "4.128.0"
}
}
+2
View File
@@ -2,6 +2,8 @@ import type { HonoApp } from '@repo/hono-helpers'
import type { SharedHonoEnv, SharedHonoVariables } from '@repo/hono-helpers/src/types'
export type Env = SharedHonoEnv & {
/** Maximum accepted binary upload size in bytes. */
MAX_UPLOAD_BYTES?: string | number
// Shared Secrets Store binding for the HS256 JWT signing key. Resolve the value
// with `await env.JWT_SECRET.get()`; all workers bind the same store so tokens
// signed by `auth` verify here.
+26 -1
View File
@@ -2,7 +2,13 @@ import { Hono } from 'hono'
import { describeRoute, openAPIRouteHandler } from 'hono-openapi'
import { useWorkersLogger } from 'workers-tagged-logger'
import { withCleanSpec, withDefaultCors, withNotFound, withOnError } from '@repo/hono-helpers'
import {
intVar,
withCleanSpec,
withDefaultCors,
withNotFound,
withOnError,
} from '@repo/hono-helpers'
import { validateAndGetAccountId } from '@repo/jwt'
import {
@@ -57,6 +63,19 @@ const UPLOAD_EXTENSION: Record<number, string> = {
5: '.inv',
}
/**
* A Worker must not accept an unbounded user-controlled blob into memory and R2.
* Operators can tune this for known room sizes, but an unset or invalid value keeps
* the safe 64 MiB default. Non-positive values are invalid rather than disabling the
* limit: a public upload endpoint must always have a finite ceiling.
*/
const DEFAULT_MAX_UPLOAD_BYTES = 64 * 1024 * 1024
function maxUploadBytes(value: unknown): number {
const configured = intVar(value, DEFAULT_MAX_UPLOAD_BYTES)
return configured > 0 ? configured : DEFAULT_MAX_UPLOAD_BYTES
}
function extensionForFileType(fileType: string): string {
return UPLOAD_EXTENSION[Number.parseInt(fileType, 10)] ?? ''
}
@@ -130,6 +149,7 @@ const app = new Hono<App>()
200: json(UploadResponse, 'The stored (or echoed) file name'),
400: json(ErrorResponse, 'Unknown/missing FileType, or neither a file nor a name'),
401: UNAUTHORIZED_RESPONSE,
413: json(ErrorResponse, 'The binary file exceeds the configured upload limit'),
},
}),
async (c) => {
@@ -143,6 +163,11 @@ const app = new Hono<App>()
const file = Object.values(body).find((v): v is File => v instanceof File)
if (file) {
const limit = maxUploadBytes(c.env.MAX_UPLOAD_BYTES)
if (file.size > limit) {
return c.json({ error: `file exceeds the ${limit}-byte upload limit` }, 413)
}
const fileType = textField(body, 'filetype') ?? '0'
const subfolder = subfolderForFileType(fileType)
if (subfolder === undefined) {
@@ -134,6 +134,24 @@ it('POST /upload 400s for a binary with an unknown/missing FileType', async () =
}
})
it('POST /upload rejects a binary above the configured size limit without storing it', async () => {
const original = env.MAX_UPLOAD_BYTES
env.MAX_UPLOAD_BYTES = '3'
try {
const res = await SELF.fetch(`${ORIGIN}/upload`, {
method: 'POST',
headers: await bearer(),
body: uploadForm('3', new Uint8Array([1, 2, 3, 4])),
})
expect(res.status).toBe(413)
expect((await res.json()) as { error: string }).toEqual({
error: 'file exceeds the 3-byte upload limit',
})
} finally {
env.MAX_UPLOAD_BYTES = original
}
})
it('POST /upload echoes an explicit name when no binary is posted', async () => {
const form = new FormData()
form.set('FileType', '3')