mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 15:09:18 -07:00
[img] fix potential xss
This commit is contained in:
@@ -303,6 +303,20 @@ async function serveStaticAsset(
|
|||||||
return new Response(asset.body, { headers })
|
return new Response(asset.body, { headers })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Headers that keep a stored object from running as a document. An upload reaches R2
|
||||||
|
* with whatever content-type the uploader claimed (`api` writes `file.type` through),
|
||||||
|
* so a `text/html` body with a `.jpg` name is served back as HTML on this origin.
|
||||||
|
* `nosniff` stops the browser upgrading an image type to HTML on its own, and the
|
||||||
|
* `sandbox` CSP denies scripts, plugins and same-origin access even when the stored
|
||||||
|
* type really is HTML or SVG — the page renders inert. Applied to every response, the
|
||||||
|
* status JSON and 304s included; there is no response here that should ever execute.
|
||||||
|
*/
|
||||||
|
const NO_EXECUTE_HEADERS = {
|
||||||
|
'x-content-type-options': 'nosniff',
|
||||||
|
'content-security-policy': "default-src 'none'; sandbox",
|
||||||
|
} as const
|
||||||
|
|
||||||
const app = new Hono<App>()
|
const app = new Hono<App>()
|
||||||
.use(
|
.use(
|
||||||
'*',
|
'*',
|
||||||
@@ -313,6 +327,12 @@ const app = new Hono<App>()
|
|||||||
release: c.env.SENTRY_RELEASE,
|
release: c.env.SENTRY_RELEASE,
|
||||||
})(c, next)
|
})(c, next)
|
||||||
)
|
)
|
||||||
|
.use('*', async (c, next) => {
|
||||||
|
await next()
|
||||||
|
for (const [name, value] of Object.entries(NO_EXECUTE_HEADERS)) {
|
||||||
|
c.res.headers.set(name, value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
.onError(withOnError())
|
.onError(withOnError())
|
||||||
.notFound(withNotFound())
|
.notFound(withNotFound())
|
||||||
|
|||||||
@@ -70,6 +70,43 @@ describe('img endpoints', () => {
|
|||||||
expect(await res.json()).toEqual({ service: 'img', status: 'ok' })
|
expect(await res.json()).toEqual({ service: 'img', status: 'ok' })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// An upload keeps whatever content-type the uploader claimed, so a stored HTML
|
||||||
|
// document would otherwise render — and run — on this origin. Every response says
|
||||||
|
// `nosniff` and carries a sandboxing CSP so the body is inert whatever it is.
|
||||||
|
it('serves every response with headers that stop a stored HTML upload executing', async () => {
|
||||||
|
const HTML_KEY = 'not-an-image.jpg'
|
||||||
|
await env.IMAGES.put(HTML_KEY, '<script>alert(1)</script>', {
|
||||||
|
httpMetadata: { contentType: 'text/html' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const html = await SELF.fetch(`${ORIGIN}/${HTML_KEY}`)
|
||||||
|
expect(html.status).toBe(200)
|
||||||
|
expect(html.headers.get('x-content-type-options')).toBe('nosniff')
|
||||||
|
expect(html.headers.get('content-security-policy')).toBe("default-src 'none'; sandbox")
|
||||||
|
|
||||||
|
// The stored object, the static asset, the fallback, a resize, and a 304.
|
||||||
|
const paths = [
|
||||||
|
`/${R2_KEY}`,
|
||||||
|
`/${CDN_NAME}`,
|
||||||
|
'/does-not-exist-anywhere.jpg',
|
||||||
|
'/3DCharades.jpg?width=128',
|
||||||
|
'/',
|
||||||
|
]
|
||||||
|
for (const path of paths) {
|
||||||
|
const res = await SELF.fetch(`${ORIGIN}${path}`)
|
||||||
|
expect(res.status, path).toBe(200)
|
||||||
|
expect(res.headers.get('x-content-type-options'), path).toBe('nosniff')
|
||||||
|
expect(res.headers.get('content-security-policy'), path).toBe("default-src 'none'; sandbox")
|
||||||
|
}
|
||||||
|
|
||||||
|
const etag = (await SELF.fetch(`${ORIGIN}/${R2_KEY}`)).headers.get('etag')
|
||||||
|
const notModified = await SELF.fetch(`${ORIGIN}/${R2_KEY}`, {
|
||||||
|
headers: { 'if-none-match': etag! },
|
||||||
|
})
|
||||||
|
expect(notModified.status).toBe(304)
|
||||||
|
expect(notModified.headers.get('x-content-type-options')).toBe('nosniff')
|
||||||
|
})
|
||||||
|
|
||||||
it('streams an image stored in R2 with its content type', async () => {
|
it('streams an image stored in R2 with its content type', async () => {
|
||||||
const res = await SELF.fetch(`${ORIGIN}/${R2_KEY}`)
|
const res = await SELF.fetch(`${ORIGIN}/${R2_KEY}`)
|
||||||
expect(res.status).toBe(200)
|
expect(res.status).toBe(200)
|
||||||
|
|||||||
Reference in New Issue
Block a user