[img] hardening

This commit is contained in:
Devin Zuczek
2026-10-22 12:21:21 -04:00
parent 3cf4b4029e
commit 605a35c36b
7 changed files with 94 additions and 34 deletions
+1 -1
View File
@@ -482,7 +482,7 @@ export const avatarRoutes = new Hono<App>({ strict: false })
'`avatar-item/<date>/<id>-thumb.png` and `avatar-item/<date>/<id>-design.png`; those ' +
'keys are the `ThumbnailImageFilename` / `DesignFilename` on the row. Each file is ' +
'identified by its bytes — the client posts them under `.bin` names — and must be a ' +
'PNG, JPEG, GIF, WebP or BMP; the key’s extension and the stored content-type come ' +
'PNG or JPEG; the key’s extension and the stored content-type come ' +
'from that signature, and anything else is refused with a 400 before either file is ' +
'written.\n\n' +
'`Price` is capped at 1000 tokens (`MAX_PLAYER_ITEM_PRICE`); one above it, or below ' +
+2 -3
View File
@@ -220,10 +220,9 @@ export const imageRoutes = new Hono<App>({ strict: false })
'`savedImageType` of 4 (profile thumbnail) additionally becomes the account’s ' +
'avatar, persisted on the account row.\n\n' +
'The file is identified by its bytes, not by the part’s filename or content-type: ' +
'it must be a PNG, JPEG, GIF, WebP or BMP, and the stored key’s extension and ' +
'it must be a PNG or JPEG, and the stored key’s extension and ' +
'content-type come from that signature. Anything else is refused with a 400 — the ' +
'`img` worker serves the object back under its stored type, so an HTML body under ' +
'an image name would otherwise be a page on the image origin.',
'`img` worker serves only those two formats, so it would never display.',
security: AUTHED,
requestBody: form(UploadImageRequest, 'The image file plus its metadata'),
responses: {
+5 -9
View File
@@ -5017,14 +5017,10 @@ describe('custom avatar items', () => {
// Both uploads landed in the image bucket under those keys.
const thumb = await env.IMAGES.get(body.Value.ThumbnailImageFilename as string)
expect(new Uint8Array((await thumb!.arrayBuffer()) as ArrayBuffer)).toEqual(
new Uint8Array([1, 2, 3])
)
expect(new Uint8Array((await thumb!.arrayBuffer()) as ArrayBuffer)).toEqual(PNG_BYTES)
expect(thumb!.httpMetadata?.contentType).toBe('image/png')
const design = await env.IMAGES.get(body.Value.DesignFilename as string)
expect(new Uint8Array((await design!.arrayBuffer()) as ArrayBuffer)).toEqual(
new Uint8Array([4, 5, 6])
)
expect(new Uint8Array((await design!.arrayBuffer()) as ArrayBuffer)).toEqual(PNG_BYTES)
// The row is the record as JSON, with the queried fields generated off it.
const row = await env.DB.prepare(
@@ -5384,14 +5380,14 @@ describe('custom avatar items', () => {
expect(badThumbnail.status).toBe(400)
expect(await badThumbnail.json()).toMatchObject({
Success: false,
Error: 'thumbnailImage is not a PNG, JPEG, GIF, WebP or BMP image',
Error: 'thumbnailImage is not a PNG or JPEG image',
})
const badDesign = await upload(PNG_BYTES, html)
expect(badDesign.status).toBe(400)
expect(await badDesign.json()).toMatchObject({
Success: false,
Error: 'design is not a PNG, JPEG, GIF, WebP or BMP image',
Error: 'design is not a PNG or JPEG image',
})
const row = await env.DB.prepare(
@@ -6974,7 +6970,7 @@ describe('images', () => {
body: fd,
})
expect(res.status).toBe(400)
expect(await res.json()).toEqual({ error: 'image is not a PNG, JPEG, GIF, WebP or BMP image' })
expect(await res.json()).toEqual({ error: 'image is not a PNG or JPEG image' })
expect((await env.IMAGES.list()).objects).toHaveLength(objectsBefore)
})