mirror of
https://github.com/layer-systems/website.git
synced 2026-09-13 06:07:29 +02:00
Fix CodeQL incomplete-sanitization in table serialization
Co-authored-by: mroxso <24775431+mroxso@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
25e75ade68
commit
262473b01b
@@ -31,9 +31,7 @@ function createEditor(): Editor {
|
||||
describe('editor schema + sanitizer', () => {
|
||||
it('rejects javascript: link hrefs end to end', () => {
|
||||
const editor = createEditor();
|
||||
editor.commands.setContent('<p><a href="javascript:alert(1)">click</a></p>', {
|
||||
contentType: 'html',
|
||||
});
|
||||
editor.commands.setContent('<p><a href="javascript:alert(1)">click</a></p>');
|
||||
// The unsafe href must not survive into the document.
|
||||
expect(editor.getHTML()).not.toContain('javascript:');
|
||||
editor.destroy();
|
||||
@@ -41,9 +39,7 @@ describe('editor schema + sanitizer', () => {
|
||||
|
||||
it('keeps safe link hrefs', () => {
|
||||
const editor = createEditor();
|
||||
editor.commands.setContent('<p><a href="https://example.com">click</a></p>', {
|
||||
contentType: 'html',
|
||||
});
|
||||
editor.commands.setContent('<p><a href="https://example.com">click</a></p>');
|
||||
expect(editor.getHTML()).toContain('https://example.com');
|
||||
editor.destroy();
|
||||
});
|
||||
@@ -51,7 +47,7 @@ describe('editor schema + sanitizer', () => {
|
||||
it('drops script content pasted as HTML', () => {
|
||||
const editor = createEditor();
|
||||
const sanitized = sanitizeHtml('<p>hello</p><script>alert(1)</script>');
|
||||
editor.commands.setContent(sanitized, { contentType: 'html' });
|
||||
editor.commands.setContent(sanitized);
|
||||
expect(editor.getText()).toContain('hello');
|
||||
expect(editor.getHTML()).not.toContain('<script');
|
||||
editor.destroy();
|
||||
@@ -59,9 +55,7 @@ describe('editor schema + sanitizer', () => {
|
||||
|
||||
it('serializes typed content to portable Markdown', () => {
|
||||
const editor = createEditor();
|
||||
editor.commands.setContent('<h1>Title</h1><p>Some <strong>bold</strong> text</p>', {
|
||||
contentType: 'html',
|
||||
});
|
||||
editor.commands.setContent('<h1>Title</h1><p>Some <strong>bold</strong> text</p>');
|
||||
expect(docToMarkdown(editor.getJSON())).toBe('# Title\n\nSome **bold** text');
|
||||
editor.destroy();
|
||||
});
|
||||
|
||||
@@ -124,6 +124,27 @@ describe('docToMarkdown', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps pipes and newlines in table cells from breaking the table', () => {
|
||||
const input = doc({
|
||||
type: 'table',
|
||||
content: [
|
||||
{
|
||||
type: 'tableRow',
|
||||
content: [
|
||||
{ type: 'tableHeader', content: [para(text('a|b'))] },
|
||||
{ type: 'tableHeader', content: [para(text('c'))] },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
// The pipe is rewritten to the full-width form so the row still parses as
|
||||
// exactly two cells.
|
||||
expect(docToMarkdown(input)).toBe('| a¦b | c |\n| --- | --- |');
|
||||
// And it round-trips back to a two-cell table.
|
||||
const parsed = markdownToDoc('| a¦b | c |\n| --- | --- |');
|
||||
expect(docToMarkdown(parsed)).toBe('| a¦b | c |\n| --- | --- |');
|
||||
});
|
||||
|
||||
it('escapes characters that would change meaning', () => {
|
||||
const input = para(text('1. not a list # not a heading *not italic*'));
|
||||
expect(docToMarkdown(doc(input))).toBe('1\\. not a list \\# not a heading \\*not italic\\*');
|
||||
|
||||
@@ -21,9 +21,9 @@ const MENTION_PATTERN = /^nostr:((npub|nprofile|note|nevent|naddr)1[02-9ac-hj-np
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function escapeText(text: string): string {
|
||||
// Escape every ASCII punctuation character. CommonMark defines a backslash
|
||||
// before ASCII punctuation as the literal character, so this is uniform,
|
||||
// always round-trips, and needs no positional special cases.
|
||||
// Escape every CommonMark-significant ASCII punctuation character. These
|
||||
// are exactly the characters a backslash escapes per the spec, so the
|
||||
// output is uniform, always round-trips, and needs no positional cases.
|
||||
return text.replace(/([!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/g, '\\$1');
|
||||
}
|
||||
|
||||
@@ -151,7 +151,12 @@ function serializeTable(node: JSONContent): string[] {
|
||||
const text = (cell.content ?? [])
|
||||
.map((block) => serializeInline(block.content))
|
||||
.join(' ')
|
||||
.replace(/\|/g, '\\|')
|
||||
// Inside a table row a pipe would break the cell boundary and a
|
||||
// newline would break the row. The inline text has already had any
|
||||
// `|` escaped to `\|`; rewrite that escaped pipe (and any bare one)
|
||||
// to the full-width `¦` so the row keeps its structural cells without
|
||||
// emitting a backslash. Newlines become a space.
|
||||
.replace(/\\?\|/g, '¦')
|
||||
.replace(/\n/g, ' ')
|
||||
.trim();
|
||||
cells.push(text);
|
||||
@@ -463,18 +468,21 @@ export function markdownToDoc(markdown: string): JSONContent {
|
||||
// Table: header row, | --- | separator, then body rows.
|
||||
if (/^\|.*\|\s*$/.test(line) && i + 1 < lines.length && /^\|[\s:|-]+\|\s*$/.test(lines[i + 1])) {
|
||||
flushParagraph();
|
||||
const parseRow = (row: string, header: boolean): JSONContent => ({
|
||||
const parseRow = (row: string, header: boolean): JSONContent => ({
|
||||
type: 'tableRow',
|
||||
content: row
|
||||
.replace(/^\|/, '')
|
||||
.replace(/\|$/, '')
|
||||
// Cells split on a bare `|`; a backslash-escaped `\|` stays inline.
|
||||
.split(/(?<!\\)\|/)
|
||||
.map((cell) => ({
|
||||
type: header ? 'tableHeader' : 'tableCell',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: parseInline(cell.replace(/\\\|/g, '|').trim()),
|
||||
// `\|` (external GFM) and `¦` (our own full-width form) both
|
||||
// restore to a literal pipe in the cell text.
|
||||
content: parseInline(cell.replace(/\\\|/g, '|').replace(/¦/g, '|').trim()),
|
||||
},
|
||||
],
|
||||
})),
|
||||
|
||||
Reference in New Issue
Block a user