mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
Every import, the catalog pin and the docs name @hanzo/app and github.com/hanzoai/app. lib/rooms.ts is lib/app.ts and components/rooms.tsx is components/app.tsx.
91 lines
3.6 KiB
TypeScript
91 lines
3.6 KiB
TypeScript
import { test, expect } from '@playwright/test'
|
|
import { marked } from 'marked'
|
|
import { destination, read } from '@hanzo/app/lib/prose'
|
|
|
|
/**
|
|
* A LINK IN A MESSAGE GOES WHERE A BROWSER WOULD GO, AND ONLY TO A SAFE PLACE.
|
|
*
|
|
* A shared chat renders the sharer's text as markdown, so a link in it is
|
|
* written by someone the reader never chose. A browser strips control
|
|
* characters and spaces from the ends of an href and tabs and newlines from
|
|
* inside it before it reads the scheme, so `\x01javascript:` runs script. The
|
|
* allowlist has to read the href the same way; these are the spellings that
|
|
* reached a `javascript:` link before it did.
|
|
*
|
|
* `components/workspace/Prose.tsx` draws a link token as an anchor exactly when
|
|
* `destination` answers, carrying the href it answers with, so the hrefs a
|
|
* message can put on the page are `destination` over every link token `read`
|
|
* produces — which is what this walks.
|
|
*/
|
|
|
|
const DANGEROUS = [
|
|
'[a](<\x01javascript:alert(1)>)',
|
|
'[a](\x01javascript:alert(1))',
|
|
'[a](<\x1fjavascript:alert(document.domain)>)',
|
|
'[a](<\u0000javascript:alert(1)>)',
|
|
'[a](javascript:alert(1))',
|
|
'[a](javascript:alert(1))',
|
|
'[a]( javascript:alert(1))',
|
|
'[a](<	javascript:alert(1)>)',
|
|
'[a]: <\x01javascript:alert(1)>\n\n[x][a]',
|
|
'<\x01javascript:alert(1)>',
|
|
'[a](javascript:alert(1))',
|
|
'[a](JaVaScRiPt:alert(1))',
|
|
'[a](javascript:alert(1))',
|
|
'[a](<\x0bjavascript:alert(1)>)',
|
|
'[a](<\x0cjavascript:alert(1)>)',
|
|
'[a](\\\x01javascript:alert(1))',
|
|
'[a](<java\tscript:alert(1)>)',
|
|
'[a](<java\nscript:alert(1)>)',
|
|
'[a](data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==)',
|
|
'[a](vbscript:msgbox(1))',
|
|
]
|
|
|
|
/** Every link a message would draw: the href and whether it opens away. */
|
|
function links(text: string): { href: string; away: boolean }[] {
|
|
const out: { href: string; away: boolean }[] = []
|
|
marked.walkTokens(read(text), (t) => {
|
|
if (t.type !== 'link') return
|
|
const to = destination(t.href)
|
|
if (to) out.push(to)
|
|
})
|
|
return out
|
|
}
|
|
|
|
/** The scheme a browser follows for an href it was handed as an attribute
|
|
* value: the URL parser's own stripping, then the parse. */
|
|
function scheme(href: string): string {
|
|
const bare = href.replace(/[\t\n\r]/g, '').replace(/^[\u0000- ]+|[\u0000- ]+$/g, '')
|
|
try {
|
|
return new URL(bare, 'https://hanzo.ai/').protocol
|
|
} catch {
|
|
return 'unparseable:'
|
|
}
|
|
}
|
|
|
|
test.describe('prose links', () => {
|
|
for (const c of DANGEROUS) {
|
|
test(`no script from ${JSON.stringify(c)}`, () => {
|
|
for (const { href } of links(c)) {
|
|
expect(['http:', 'https:', 'mailto:', 'tel:'], `${JSON.stringify(c)} drew ${JSON.stringify(href)}`).toContain(scheme(href))
|
|
}
|
|
})
|
|
}
|
|
|
|
// A link that leaves the page opens away from it — a new tab, no opener, no
|
|
// referrer — however its host is spelled. (Regression: `/\host` resolves to
|
|
// another site and was treated as a link back into this one.)
|
|
for (const href of ['//evil.example/x', '/\\evil.example/x', 'https://evil.example/', 'HTTPS://EVIL.EXAMPLE']) {
|
|
test(`${JSON.stringify(href)} opens away`, () => {
|
|
expect(links(`[a](${href})`)).toEqual([expect.objectContaining({ away: true })])
|
|
})
|
|
}
|
|
|
|
test('ordinary links still link', () => {
|
|
expect(links('[docs](https://hanzo.ai/docs)')).toEqual([{ href: 'https://hanzo.ai/docs', away: true }])
|
|
expect(links('[mail](mailto:[email protected])')).toEqual([{ href: 'mailto:[email protected]', away: true }])
|
|
expect(links('[home](/chat)')).toEqual([{ href: '/chat', away: false }])
|
|
expect(links('[top](#top)')).toEqual([{ href: '#top', away: false }])
|
|
})
|
|
})
|