feat(demo): refuse a data: URI in any write body (DM25)
All checks were successful
PR Checks / bot-tests (pull_request) Successful in 45s
PR Checks / client-build (pull_request) Successful in 1m2s
PR Checks / server-tests (pull_request) Successful in 6m4s

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:
2026-10-27 13:13:51 -05:00
parent 3b9998539f
commit 61feca50a8
4 changed files with 130 additions and 1 deletions

View File

@@ -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`)

View File

@@ -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="&#100;ata:image/png;base64,AAAA">',
'<img src="&#x64;&#x61;&#x74;&#x61;&#x3a;image/png;base64,AAAA">',
'<img src="data&colon;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 () => {