feat(demo): refuse a data: URI in any write body (DM25)
Uploads are locked hard on the demo, but an image pasted into rich text does not use multipart: it is saved as a data: URI inside the JSON body. refuseDataUri, mounted beside refuseMultipart, refuses any POST/PUT/PATCH/ DELETE whose body has a data: URI in any string, on any route, core's or a module's, with the usual 403 DEMO_LOCKED. Each string is first read the way a browser reads an attribute (HTML character references decoded, tabs and newlines removed), so data: and a data: split by a newline are refused too. "metadata:" and prose such as "Data: none, yet" pass. Reads are never checked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -64,6 +64,18 @@ test('an upload is refused before multer or auth sees it', async () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('a pasted data: image is refused before auth sees it, on an open route', async () => {
|
||||
await withServer(async (base) => {
|
||||
const res = await fetch(`${base}/api/v1/admin/posts`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title: 'x', body: '<p><img src="data:image/png;base64,iVBORw0KGgo="></p>' }),
|
||||
})
|
||||
assert.equal(res.status, 403)
|
||||
assert.equal((await res.json()).code, 'DEMO_LOCKED')
|
||||
})
|
||||
})
|
||||
|
||||
test('every response carries noindex, and robots.txt disallows everything', async () => {
|
||||
await withServer(async (base) => {
|
||||
const health = await fetch(`${base}/api/health`)
|
||||
|
||||
@@ -279,6 +279,74 @@ test('any multipart request is refused before a router sees it, wherever it is s
|
||||
})
|
||||
})
|
||||
|
||||
// DM25: a data: URI in any string of a write's body is an upload too.
|
||||
async function withDataUriLock(fn) {
|
||||
const app = await startApp((a) => {
|
||||
a.use(locks.refuseDataUri)
|
||||
a.all('*', (req, res) => res.json({ reached: true }))
|
||||
})
|
||||
try {
|
||||
return await fn((method, path, body) =>
|
||||
fetch(`${app.url}/api/v1${path}`, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
}))
|
||||
} finally {
|
||||
await app.close()
|
||||
}
|
||||
}
|
||||
|
||||
test('a data: URI anywhere in a write body is refused, on any route and any write method', async () => {
|
||||
await withDataUriLock(async (req) => {
|
||||
const img = '<p>hi</p><img src="data:image/png;base64,iVBORw0KGgo=">'
|
||||
await expectLocked(await req('POST', '/admin/posts', { title: 't', body: img }))
|
||||
await expectLocked(await req('PUT', '/admin/wiki/1', { content: img }))
|
||||
await expectLocked(await req('PATCH', '/rust/anything', { deep: [{ x: { y: [img] } }] }))
|
||||
await expectLocked(await req('PUT', '/admin/settings', { hero_layout_draft: JSON.stringify([{ src: 'data:image/webp;base64,UklG' }]) }))
|
||||
await expectLocked(await req('DELETE', '/player/teams/x/forum/1', { reason: img }))
|
||||
})
|
||||
})
|
||||
|
||||
test('every data: URI shape a browser would load is refused', async () => {
|
||||
const shapes = [
|
||||
'DATA:image/png;base64,AAAA',
|
||||
'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"/>',
|
||||
'data:text/html;base64,PHNjcmlwdD4=',
|
||||
'data:,hello',
|
||||
'data:;base64,AAAA',
|
||||
'data: image/png;base64,AAAA',
|
||||
'<div style="background:url(data:image/gif;base64,R0lG)">',
|
||||
'<img src="data:image/png;base64,AAAA">',
|
||||
'<img src="data:image/png;base64,AAAA">',
|
||||
'<img src="data:image/png;base64,AAAA">',
|
||||
'<img src="da\nta:image/png;base64,AAAA">',
|
||||
'<img src="data:im\tage/png;base64,AAAA">',
|
||||
]
|
||||
for (const s of shapes) assert.equal(locks.hasDataUri({ body: s }), true, s)
|
||||
})
|
||||
|
||||
test('prose that mentions data, reads, and bodies without strings pass', async () => {
|
||||
for (const s of ['metadata: none', 'Data: none, yet', 'the data: it is fine', 'see https://example.com/data:x', 'mydata:image/png,x']) {
|
||||
assert.equal(locks.hasDataUri({ body: s }), false, s)
|
||||
}
|
||||
assert.equal(locks.hasDataUri(undefined), false)
|
||||
assert.equal(locks.hasDataUri({ n: 1, ok: true, list: [null, 2] }), false)
|
||||
await withDataUriLock(async (req) => {
|
||||
await expectReached(await req('POST', '/admin/posts', { title: 'json is fine', body: '<p>metadata: none</p>' }))
|
||||
})
|
||||
// A read is never refused, whatever it carries.
|
||||
const app = await startApp((a) => {
|
||||
a.use(locks.refuseDataUri)
|
||||
a.all('*', (req, res) => res.json({ reached: true }))
|
||||
})
|
||||
try {
|
||||
await expectReached(await fetch(`${app.url}/api/v1/public/settings?x=data:image/png;base64,AAAA`))
|
||||
} finally {
|
||||
await app.close()
|
||||
}
|
||||
})
|
||||
|
||||
// ── Module locks ───────────────────────────────────────────────────────────
|
||||
|
||||
test("a module's declared locks are refused alongside core's", async () => {
|
||||
|
||||
Reference in New Issue
Block a user