Full conversation
@@ -380,7 +380,7 @@Subject context
-
+
Reply
@@ -412,7 +412,7 @@Full conversation
@@ -455,7 +455,7 @@Changed files
diff --git a/frontend/markdown.js b/frontend/markdown.js index c2e99ce..24d6c07 100644 --- a/frontend/markdown.js +++ b/frontend/markdown.js @@ -13,17 +13,95 @@ })[character]); } - return function renderMarkdown(raw) { - return escapeHtml(raw) - .replace(/^#{1,6}\s.*$/gm, (line) => { - const level = line.match(/^(#{1,6})/)[1].length; - return '' + body + '');
+ return marker;
+ });
+ rendered = rendered
+ .replace(/\[([^\]\n]+)\]\(([^\s)]+)(?:\s+"[^&]*?")?\)/g, (_match, label, escapedUrl) => {
+ const url = escapedUrl.replace(/&/g, '&');
+ const safe = /^(https?:|mailto:)/i.test(url) || /^(\/|#|\.\/|\.\.\/)/.test(url);
+ if (!safe) return label;
+ return '' + label + '';
})
.replace(/\*\*(.+?)\*\*/g, '$1')
- .replace(/\*(.+?)\*/g, '$1')
- .replace(/`([^`]+)`/g, '$1')
- .replace(/^-\s?(.+)$/gm, '- $1
'); + .replace(/(^|[^*])\*([^*\n]+)\*/g, '$1$2'); + return rendered.replace(/\u0000CODE(\d+)\u0000/g, (_match, index) => code[Number(index)]); + } + + function renderList(lines) { + const taskList = lines.every(line => /^[-*+]\s+\[[ xX]\]\s+/.test(line)); + const items = lines.map(line => { + let body = line.replace(/^[-*+]\s+/, ''); + if (!taskList) return '
- ' + items + '
' + escapeHtml(body.join('\n')) + '');
+ continue;
+ }
+ const heading = line.match(/^(#{1,6})\s+(.+)$/);
+ if (heading) {
+ const level = heading[1].length;
+ output.push(''); + continue; + } + if (/^[-*+]\s+/.test(line)) { + const list = []; + while (index < lines.length && /^[-*+]\s+/.test(lines[index])) { + list.push(lines[index]); + index += 1; + } + output.push(renderList(list)); + continue; + } + const paragraph = []; + while (index < lines.length && lines[index].trim() && + !/^```/.test(lines[index]) && !/^(#{1,6})\s+/.test(lines[index]) && + !/^>\s?/.test(lines[index]) && !/^[-*+]\s+/.test(lines[index])) { + paragraph.push(lines[index]); + index += 1; + } + output.push('' + renderInline(quote.join('\n')).replace(/\n/g, '
') + '
' + renderInline(paragraph.join('\n')).replace(/\n/g, '
') + '
Plan
" + 'Ship this
- ' + ' Tested ' + '
- Released
print('ready')'
+ )
+
+
+def test_markdown_renderer_allows_only_safe_links_and_keeps_html_inert():
+ rendered = render_markdown(
+ "[Forge](https://forge.example/work?q=1&safe=yes) "
+ "[relative](/git/org/repo/issues/1) "
+ "[attack](javascript:alert(1))