mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-09 07:01:27 -07:00
[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:
@@ -20,7 +20,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",
|
||||
@@ -32,6 +32,6 @@
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "26.0.1",
|
||||
"vitest": "4.1.9",
|
||||
"wrangler": "4.105.0"
|
||||
"wrangler": "4.128.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,15 @@ describe('auth-gated endpoints', () => {
|
||||
expect(res.status).toBe(401)
|
||||
})
|
||||
|
||||
test('GET /account/me rejects a signed token with a non-canonical account subject', async () => {
|
||||
for (const sub of ['42junk', '42.5', '042', '-42', '9007199254740992']) {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/account/me`, {
|
||||
headers: await bearer(sub),
|
||||
})
|
||||
expect(res.status, sub).toBe(401)
|
||||
}
|
||||
})
|
||||
|
||||
test('GET /account/me returns the self account with a valid token', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/account/me`, { headers: await bearer() })
|
||||
expect(res.status).toBe(200)
|
||||
@@ -160,7 +169,7 @@ describe('auth-gated endpoints', () => {
|
||||
username: 'Player42',
|
||||
personalPronouns: 0,
|
||||
identityFlags: 0,
|
||||
availableUsernameChanges: 1,
|
||||
availableUsernameChanges: 3,
|
||||
// An unset email is "", not null — the client reads it as a string, and the
|
||||
// hub frame this DTO also rides drops null values outright.
|
||||
email: '',
|
||||
@@ -235,7 +244,7 @@ describe('auth-gated endpoints', () => {
|
||||
expect(body.value).toBe('')
|
||||
})
|
||||
|
||||
test('PUT /account/me/username changes the name, decrements the counter, then blocks', async () => {
|
||||
test('PUT /account/me/username allows three changes, decrements the counter, then blocks', async () => {
|
||||
const headers = {
|
||||
...(await bearer('892')),
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
@@ -260,11 +269,27 @@ describe('auth-gated endpoints', () => {
|
||||
await exports.default.fetch(`${ORIGIN}/account/me`, { headers: await bearer('892') })
|
||||
).json()) as { username: string; availableUsernameChanges: number }
|
||||
expect(me.username).toBe('coachx')
|
||||
expect(me.availableUsernameChanges).toBe(0)
|
||||
expect(me.availableUsernameChanges).toBe(2)
|
||||
|
||||
// A second change is blocked — no changes remaining (still HTTP 200).
|
||||
// The second and third changes consume the rest of the account's allowance.
|
||||
for (const username of ['coachy', 'coachz']) {
|
||||
const changed = await exports.default.fetch(`${ORIGIN}/account/me/username`, {
|
||||
...form({ username }),
|
||||
headers,
|
||||
})
|
||||
expect(changed.status).toBe(200)
|
||||
expect(((await changed.json()) as { success: boolean }).success).toBe(true)
|
||||
}
|
||||
|
||||
const exhausted = (await (
|
||||
await exports.default.fetch(`${ORIGIN}/account/me`, { headers: await bearer('892') })
|
||||
).json()) as { username: string; availableUsernameChanges: number }
|
||||
expect(exhausted.username).toBe('coachz')
|
||||
expect(exhausted.availableUsernameChanges).toBe(0)
|
||||
|
||||
// A fourth change is blocked — no changes remaining (still HTTP 200).
|
||||
const blocked = await exports.default.fetch(`${ORIGIN}/account/me/username`, {
|
||||
...form({ username: 'coachy' }),
|
||||
...form({ username: 'coachq' }),
|
||||
headers,
|
||||
})
|
||||
expect(blocked.status).toBe(200)
|
||||
@@ -565,12 +590,12 @@ describe('name, email and bio validation', () => {
|
||||
expect(body.value).toBe('')
|
||||
}
|
||||
|
||||
// The rationed change must NOT be spent by a refusal: an account starts with one,
|
||||
// A rationed change must NOT be spent by a refusal: an account starts with three,
|
||||
// and burning it on a typo would leave the player stuck with a name they never had.
|
||||
const me = (await (
|
||||
await exports.default.fetch(`${ORIGIN}/account/me`, { headers: await bearer('8801') })
|
||||
).json()) as { availableUsernameChanges: number }
|
||||
expect(me.availableUsernameChanges).toBe(1)
|
||||
expect(me.availableUsernameChanges).toBe(3)
|
||||
|
||||
// 50 is the client's own cap, so a name that long has to be accepted.
|
||||
const ok = await exports.default.fetch(`${ORIGIN}/account/me/username`, {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,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",
|
||||
@@ -34,6 +34,6 @@
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "26.0.1",
|
||||
"vitest": "4.1.9",
|
||||
"wrangler": "4.105.0"
|
||||
"wrangler": "4.128.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,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",
|
||||
@@ -33,6 +33,6 @@
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "26.0.1",
|
||||
"vitest": "4.1.9",
|
||||
"wrangler": "4.105.0"
|
||||
"wrangler": "4.128.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/hono-helpers": "workspace:*",
|
||||
"hono": "4.12.27",
|
||||
"hono": "4.13.5",
|
||||
"workers-tagged-logger": "1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -25,6 +25,6 @@
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "26.0.1",
|
||||
"vitest": "4.1.9",
|
||||
"wrangler": "4.105.0"
|
||||
"wrangler": "4.128.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,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",
|
||||
@@ -33,6 +33,6 @@
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "26.0.1",
|
||||
"vitest": "4.1.9",
|
||||
"wrangler": "4.105.0"
|
||||
"wrangler": "4.128.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,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",
|
||||
@@ -33,6 +33,6 @@
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "26.0.1",
|
||||
"vitest": "4.1.9",
|
||||
"wrangler": "4.105.0"
|
||||
"wrangler": "4.128.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"@repo/hono-helpers": "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",
|
||||
@@ -30,6 +30,6 @@
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "26.0.1",
|
||||
"vitest": "4.1.9",
|
||||
"wrangler": "4.105.0"
|
||||
"wrangler": "4.128.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/hono-helpers": "workspace:*",
|
||||
"hono": "4.12.27",
|
||||
"hono": "4.13.5",
|
||||
"workers-tagged-logger": "1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -25,6 +25,6 @@
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "26.0.1",
|
||||
"vitest": "4.1.9",
|
||||
"wrangler": "4.105.0"
|
||||
"wrangler": "4.128.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"@repo/hono-helpers": "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",
|
||||
@@ -30,6 +30,6 @@
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "26.0.1",
|
||||
"vitest": "4.1.9",
|
||||
"wrangler": "4.105.0"
|
||||
"wrangler": "4.128.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,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",
|
||||
@@ -34,6 +34,6 @@
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "26.0.1",
|
||||
"vitest": "4.1.9",
|
||||
"wrangler": "4.105.0"
|
||||
"wrangler": "4.128.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@repo/hono-helpers": "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",
|
||||
@@ -32,6 +32,6 @@
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "26.0.1",
|
||||
"vitest": "4.1.9",
|
||||
"wrangler": "4.105.0"
|
||||
"wrangler": "4.128.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,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",
|
||||
@@ -33,6 +33,6 @@
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "26.0.1",
|
||||
"vitest": "4.1.9",
|
||||
"wrangler": "4.105.0"
|
||||
"wrangler": "4.128.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/hono-helpers": "workspace:*",
|
||||
"hono": "4.12.27",
|
||||
"hono": "4.13.5",
|
||||
"workers-tagged-logger": "1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -25,6 +25,6 @@
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "26.0.1",
|
||||
"vitest": "4.1.9",
|
||||
"wrangler": "4.105.0"
|
||||
"wrangler": "4.128.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"@repo/domain": "workspace:*",
|
||||
"@repo/hono-helpers": "workspace:*",
|
||||
"@repo/jwt": "workspace:*",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,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",
|
||||
@@ -33,6 +33,6 @@
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "26.0.1",
|
||||
"vitest": "4.1.9",
|
||||
"wrangler": "4.105.0"
|
||||
"wrangler": "4.128.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/hono-helpers": "workspace:*",
|
||||
"hono": "4.12.27",
|
||||
"hono": "4.13.5",
|
||||
"workers-tagged-logger": "1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -25,6 +25,6 @@
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "26.0.1",
|
||||
"vitest": "4.1.9",
|
||||
"wrangler": "4.105.0"
|
||||
"wrangler": "4.128.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"dependencies": {
|
||||
"@repo/hono-helpers": "workspace:*",
|
||||
"@repo/jwt": "workspace:*",
|
||||
"hono": "4.12.27",
|
||||
"hono": "4.13.5",
|
||||
"workers-tagged-logger": "1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -26,6 +26,6 @@
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "26.0.1",
|
||||
"vitest": "4.1.9",
|
||||
"wrangler": "4.105.0"
|
||||
"wrangler": "4.128.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/hono-helpers": "workspace:*",
|
||||
"hono": "4.12.27",
|
||||
"hono": "4.13.5",
|
||||
"workers-tagged-logger": "1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -25,6 +25,6 @@
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "26.0.1",
|
||||
"vitest": "4.1.9",
|
||||
"wrangler": "4.105.0"
|
||||
"wrangler": "4.128.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"dependencies": {
|
||||
"@repo/hono-helpers": "workspace:*",
|
||||
"@repo/jwt": "workspace:*",
|
||||
"hono": "4.12.27",
|
||||
"hono": "4.13.5",
|
||||
"workers-tagged-logger": "1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -26,6 +26,6 @@
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "26.0.1",
|
||||
"vitest": "4.1.9",
|
||||
"wrangler": "4.105.0"
|
||||
"wrangler": "4.128.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,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",
|
||||
@@ -33,6 +33,6 @@
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "26.0.1",
|
||||
"vitest": "4.1.9",
|
||||
"wrangler": "4.105.0"
|
||||
"wrangler": "4.128.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,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",
|
||||
@@ -33,6 +33,6 @@
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "26.0.1",
|
||||
"vitest": "4.1.9",
|
||||
"wrangler": "4.105.0"
|
||||
"wrangler": "4.128.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
"@repo/domain": "workspace:*",
|
||||
"@repo/hono-helpers": "workspace:*",
|
||||
"@repo/jwt": "workspace:*",
|
||||
"@scalar/api-reference": "1.63.0",
|
||||
"hono": "4.12.27",
|
||||
"@scalar/api-reference": "1.67.0",
|
||||
"hono": "4.13.5",
|
||||
"react": "19.2.7",
|
||||
"react-dom": "19.2.7",
|
||||
"workers-tagged-logger": "1.0.1"
|
||||
@@ -36,6 +36,6 @@
|
||||
"@vitejs/plugin-react": "5.2.0",
|
||||
"vite": "6.4.3",
|
||||
"vitest": "4.1.9",
|
||||
"wrangler": "4.105.0"
|
||||
"wrangler": "4.128.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user