import assert from 'node:assert/strict'
import { test } from 'node:test'
import { chromium } from 'playwright'
import { createServer } from 'vite'
test('untrusted Markdown image URLs cannot request their URL during render', async () => {
const server = await createServer({ configFile: false, server: { host: '127.0.0.1', port: 0 }, logLevel: 'error' })
let browser
try {
await server.listen()
const address = server.httpServer.address()
assert.ok(address && typeof address !== 'string')
browser = await chromium.launch({ executablePath: process.env.PLAYWRIGHT_CHROMIUM_BIN })
const page = await browser.newPage()
const attempted = []
// Establish the Vite origin without booting the app or its dev WebSocket.
await page.goto(`http://127.0.0.1:${address.port}/src/plugin/markdown-renderer-impl.ts`)
await page.evaluate(() => import('/src/plugin/markdown-renderer-impl.ts'))
await page.route('**/*', async route => {
attempted.push(route.request().url())
await route.abort()
})
// Positive control: this browser/route must see a real image fetch.
await page.evaluate(() => {
const img = document.createElement('img')
img.src = 'https://example.test/control'
document.body.appendChild(img)
})
await page.waitForFunction(() => document.querySelector('img')?.complete)
assert.deepEqual(attempted, ['https://example.test/control'])
attempted.length = 0
const result = await page.evaluate(async () => {
const { renderMarkdownToElement, registerPostProcessor, clearAllPostProcessors } =
await import('/src/plugin/markdown-renderer-impl.ts')
const el = document.createElement('section')
document.body.appendChild(el)
window.__markdownProofExecuted = false
let processors = 0
registerPostProcessor('browser-proof', () => { processors++ })
try {
await renderMarkdownToElement(
'> [!note] Safe **title**\n> \n\n\n\n\n\n\n\n![reference][ref]\n\n[ref]: https://example.test/reference\n\n
\n\n\n\n\n\n\n\n[unsafe](javascript:window.__markdownProofExecuted=true)\n\n[link](https://example.org/)',
el, '/untrusted.md', null,
)
return {
callout: !!el.querySelector('.callout'),
title: el.querySelector('.callout-title')?.textContent,
text: el.textContent,
images: el.querySelectorAll('img').length,
link: el.querySelector('a')?.getAttribute('href'),
unsafeLink: [...el.querySelectorAll('a')].find(a => a.textContent === 'unsafe')?.getAttribute('href'),
activeNodes: el.querySelectorAll('iframe, svg, math, script, img').length,
executed: window.__markdownProofExecuted,
processors,
}
} finally {
clearAllPostProcessors()
}
})
await page.waitForTimeout(250)
assert.deepEqual(attempted, [])
assert.equal(result.callout, true)
assert.equal(result.images, 0)
assert.equal(result.activeNodes, 0)
assert.equal(result.executed, false)
assert.equal(result.unsafeLink, '#')
assert.equal(await page.evaluate(() => { document.querySelector('a[href="#"]')?.click(); return window.__markdownProofExecuted }), false)
assert.ok(page.url().startsWith(`http://127.0.0.1:${address.port}/`))
assert.equal(result.link, '#')
assert.match(result.title, /Safe title/)
assert.match(result.text, /private/)
assert.ok(result.processors > 0)
} finally {
await browser?.close()
await server.close()
}
})