|
#!/usr/bin/env bash |
|
# Unit tests for the inline and block parsing rules. |
|
# Run: test/units.sh (or test/run.sh for everything) |
|
|
|
set -uo pipefail |
|
source "$(dirname "${BASH_SOURCE[0]}")/assert.bash" |
|
|
|
REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) |
|
AWK="${AWK:-awk}" |
|
SED="${SED:-sed}" |
|
|
|
suite "markdown: intraword underscores" |
|
|
|
# The deep-link id is stripped so these assertions can state the markup they |
|
# are about. Each is checking how a span of text is parsed, not that a |
|
# paragraph carries an anchor -- that has tests of its own, and pinning a |
|
# content hash in every one of these would make them unreadable and would |
|
# break them all again on any wording change. |
|
md_unsafe() { printf '%s\n' "$1" | "$AWK" -v UNSAFE_HTML=1 -f "$REPO_ROOT/markdown.awk"; } |
|
md() { printf '%s\n' "$1" | "$AWK" -f "$REPO_ROOT/markdown.awk" \ |
|
| "$SED" 's/<p id="p-[0-9a-f]*\(-[0-9]*\)\{0,1\}">/<p>/g'; } |
|
|
|
it "an underscore inside a word is literal" |
|
assert_eq "$(md 'node_modules here')" "<p>node_modules here</p>" |
|
|
|
it "snake_case survives intact" |
|
assert_eq "$(md 'snake_case_name')" "<p>snake_case_name</p>" |
|
|
|
it "a leading underscore still opens emphasis" |
|
assert_eq "$(md 'a _real_ emphasis')" "<p>a <em>real</em> emphasis</p>" |
|
|
|
it "intraword and real emphasis coexist on one line" |
|
assert_eq "$(md 'x_y and _z_')" "<p>x_y and <em>z</em></p>" |
|
|
|
it "asterisk emphasis is unaffected" |
|
assert_eq "$(md 'a *starred* word')" "<p>a <em>starred</em> word</p>" |
|
|
|
it "intraword asterisks still emphasise, as in CommonMark" |
|
assert_eq "$(md 'a*b*c')" "<p>a<em>b</em>c</p>" |
|
|
|
it "bold with underscores still works" |
|
assert_eq "$(md 'a __bold__ word')" "<p>a <strong>bold</strong> word</p>" |
|
|
|
it "no stray em is emitted for an identifier" |
|
assert_not_contains "$(md 'multi_word_ident text')" "<em>" |
|
|
|
suite "markdown: fenced code blocks" |
|
|
|
# Whitespace is significant inside <pre>, so the tags are joined to the first |
|
# and last content lines. A newline after the opening tag would render as a |
|
# blank first line in the block. |
|
it "a plain fence produces a code block" |
|
assert_eq "$(md '``` |
|
echo hi |
|
```')" "<pre><code>echo hi</code></pre>" |
|
|
|
it "no blank line is introduced at the top of a block" |
|
assert_not_contains "$(md '``` |
|
echo hi |
|
```')" "<code> |
|
" |
|
|
|
it "no blank line at the bottom either" |
|
assert_not_contains "$(md '``` |
|
echo hi |
|
```')" " |
|
</code>" |
|
|
|
it "interior blank lines are preserved" |
|
assert_contains "$(md '``` |
|
one |
|
|
|
two |
|
```')" "one |
|
|
|
two" |
|
|
|
it "the fence markers are not in the output" |
|
assert_not_contains "$(md '``` |
|
echo hi |
|
```')" '`' |
|
|
|
it "a language tag becomes a class" |
|
assert_contains "$(md '```bash |
|
echo hi |
|
```')" 'class="language-bash"' |
|
|
|
it "multiple lines are preserved separately" |
|
out=$(md '``` |
|
line one |
|
line two |
|
```') |
|
assert_contains "$out" "line one |
|
line two" |
|
|
|
it "HTML inside a fence is escaped" |
|
assert_contains "$(md '``` |
|
<b>x</b> & y |
|
```')" "<b>x</b> & y" |
|
|
|
it "markdown inside a fence is left alone" |
|
assert_contains "$(md '``` |
|
*not emphasis* and _not_ either |
|
```')" "*not emphasis* and _not_ either" |
|
|
|
it "smart quotes do not touch fenced content" |
|
assert_contains "$(md '``` |
|
say "quoted" here |
|
```')" 'say "quoted" here' |
|
|
|
it "text around a fence still renders" |
|
out=$(md 'Before. |
|
|
|
``` |
|
code |
|
``` |
|
|
|
After.') |
|
assert_contains "$out" "<p>Before.</p>" |
|
|
|
it "text after a fence is a separate paragraph" |
|
assert_contains "$out" "<p>After.</p>" |
|
|
|
it "a tilde fence works too" |
|
assert_contains "$(md '~~~ |
|
echo hi |
|
~~~')" "<pre><code>" |
|
|
|
it "an empty fence is handled" |
|
assert_contains "$(md '``` |
|
```')" "<pre><code>" |
|
|
|
it "indented code blocks still work" |
|
assert_contains "$(md 'Text: |
|
|
|
indented')" "<pre><code>" |
|
|
|
suite "markdown: reference links" |
|
|
|
it "an explicit label resolves" |
|
assert_contains "$(md 'See [the docs][d]. |
|
|
|
[d]: http://example.com')" '<a href="http://example.com">the docs</a>' |
|
|
|
it "the definition does not appear in the output" |
|
assert_not_contains "$(md 'See [the docs][d]. |
|
|
|
[d]: http://example.com')" 'http://example.com<' |
|
|
|
it "an implicit label reuses the link text" |
|
assert_contains "$(md 'See [docs][]. |
|
|
|
[docs]: http://example.com')" '<a href="http://example.com">docs</a>' |
|
|
|
it "a shortcut reference resolves" |
|
assert_contains "$(md 'See [docs]. |
|
|
|
[docs]: http://example.com')" '<a href="http://example.com">docs</a>' |
|
|
|
it "labels are case-insensitive" |
|
assert_contains "$(md 'See [Docs][D]. |
|
|
|
[d]: http://example.com')" 'href="http://example.com"' |
|
|
|
it "a title is carried through" |
|
assert_contains "$(md 'See [x][d]. |
|
|
|
[d]: http://example.com "The Title"')" 'title="The Title"' |
|
|
|
it "a definition before its use resolves" |
|
assert_contains "$(md '[d]: http://example.com |
|
|
|
See [x][d].')" 'href="http://example.com"' |
|
|
|
it "an unresolved reference is left as literal text" |
|
out=$(md 'See [x][nope].') |
|
assert_not_contains "$out" '<a href="">' |
|
|
|
it "inline links are unaffected" |
|
assert_contains "$(md 'See [x](http://example.com).')" '<a href="http://example.com">x</a>' |
|
|
|
it "footnotes still work alongside references" |
|
out=$(md 'Text[^1] and [x][d]. |
|
|
|
[^1]: note |
|
[d]: http://example.com') |
|
assert_contains "$out" 'href="http://example.com"' |
|
|
|
it "the footnote still renders" |
|
assert_contains "$out" 'fn-item' |
|
|
|
it "a definition inside a fence is literal" |
|
assert_contains "$(md '``` |
|
[d]: http://example.com |
|
```')" '[d]: http://example.com' |
|
|
|
suite "markdown: MMD metadata block" |
|
|
|
it "a metadata pair is wrapped, not stripped" |
|
assert_contains "$(md 'Series: Web Typography |
|
|
|
# Title')" 'data-key="Series"' |
|
|
|
it "the value is exposed as an attribute" |
|
assert_contains "$(md 'Series: Web Typography |
|
|
|
# Title')" 'data-value="Web Typography"' |
|
|
|
it "the original text is preserved verbatim" |
|
assert_contains "$(md 'Series: Web Typography |
|
|
|
# Title')" '>Series: Web Typography</p>' |
|
|
|
it "the block carries a class the theme can target" |
|
assert_contains "$(md 'Series: Web Typography |
|
|
|
# Title')" 'class="meta"' |
|
|
|
it "the body still renders" |
|
# Headings carry an id now, so this asserts the text rather than the exact tag. |
|
assert_contains "$(md 'Series: Web Typography |
|
|
|
# Title')" ">Title</h1>" |
|
|
|
it "multiple keys each get their own element" |
|
out=$(md 'Series: Web Typography |
|
Author: Pat |
|
|
|
# Title') |
|
assert_contains "$out" 'data-key="Series"' |
|
|
|
it "the second key is wrapped too" |
|
assert_contains "$out" 'data-key="Author"' |
|
|
|
it "tags are wrapped like any other key" |
|
assert_contains "$(md 'tags: alpha beta |
|
|
|
# Title')" 'data-key="tags"' |
|
|
|
it "a multi-word key is preserved verbatim" |
|
assert_contains "$(md 'Base Header Level: 2 |
|
|
|
# Title')" 'data-key="Base Header Level"' |
|
|
|
# MultiMarkdown makes no attempt to tell metadata from prose that happens to |
|
# start Key: value -- position is the whole rule. Verified against |
|
# multimarkdown 6.6.0, which renders "Note: this is prose." at the top of a |
|
# file as <meta name="note">. Matching that rather than inventing a smarter |
|
# heuristic keeps the parser predictable for anyone who knows MMD. |
|
it "a leading Key: value line is metadata even if it reads as prose" |
|
assert_contains "$(md 'Note: this is ordinary prose. |
|
|
|
# Title')" 'data-key="Note"' |
|
|
|
it "such a line keeps its text, so nothing is lost either way" |
|
assert_contains "$(md 'Note: this is ordinary prose. |
|
|
|
# Title')" '>Note: this is ordinary prose.</p>' |
|
|
|
it "a block below the top of the file is prose" |
|
assert_not_contains "$(md '# Title |
|
|
|
Series: Web Typography')" 'data-key="Series"' |
|
|
|
it "a document with no metadata emits no meta element" |
|
assert_not_contains "$(md '# Title |
|
|
|
Body.')" 'class="meta"' |
|
|
|
it "attribute values are escaped" |
|
assert_contains "$(md 'Series: A & B <x> |
|
|
|
# Title')" 'data-value="A & B <x>"' |
|
|
|
suite "markdown: tables" |
|
|
|
it "a basic table renders as a table" |
|
assert_contains "$(md '| A | B | |
|
|---|---| |
|
| 1 | 2 |')" "<table>" |
|
|
|
it "the header row becomes thead" |
|
assert_contains "$(md '| A | B | |
|
|---|---| |
|
| 1 | 2 |')" "<thead>" |
|
|
|
it "header cells are th" |
|
assert_contains "$(md '| A | B | |
|
|---|---| |
|
| 1 | 2 |')" "<th" |
|
|
|
it "body cells are td" |
|
assert_contains "$(md '| A | B | |
|
|---|---| |
|
| 1 | 2 |')" "<td" |
|
|
|
it "the separator row is not rendered" |
|
assert_not_contains "$(md '| A | B | |
|
|---|---| |
|
| 1 | 2 |')" "---" |
|
|
|
it "cell text is preserved" |
|
out=$(md '| A | B | |
|
|---|---| |
|
| 1 | 2 |') |
|
assert_contains "$out" "A" |
|
|
|
it "left alignment is applied" |
|
assert_contains "$(md '| L | |
|
|:--| |
|
| 1 |')" "text-align:left" |
|
|
|
it "centre alignment is applied" |
|
assert_contains "$(md '| C | |
|
|:-:| |
|
| 1 |')" "text-align:center" |
|
|
|
it "right alignment is applied" |
|
assert_contains "$(md '| R | |
|
|--:| |
|
| 1 |')" "text-align:right" |
|
|
|
it "pipes at the row edges are optional" |
|
assert_contains "$(md 'A | B |
|
--- | --- |
|
1 | 2')" "<table>" |
|
|
|
it "inline markdown inside a cell is parsed" |
|
assert_contains "$(md '| **bold** | |
|
|---| |
|
| a |')" "<strong>bold</strong>" |
|
|
|
it "a link inside a cell is parsed" |
|
assert_contains "$(md '| x | |
|
|---| |
|
| [y](http://e.com) |')" '<a href="http://e.com">y</a>' |
|
|
|
it "multiple body rows all render" |
|
out=$(md '| A | |
|
|---| |
|
| 1 | |
|
| 2 |') |
|
assert_eq "$(printf '%s' "$out" | grep -c '<td')" "2" |
|
|
|
it "the table closes" |
|
assert_contains "$(md '| A | |
|
|---| |
|
| 1 |')" "</table>" |
|
|
|
it "text after a table is a separate paragraph" |
|
assert_contains "$(md '| A | |
|
|---| |
|
| 1 | |
|
|
|
After.')" "<p>After.</p>" |
|
|
|
it "a pipe in prose is not a table" |
|
assert_not_contains "$(md 'a | b in ordinary prose')" "<table>" |
|
|
|
# A cell must handle raw HTML exactly as prose does. That used to mean both |
|
# passed it through; it now means both escape it unless the caller asks for |
|
# passthrough. What matters is that a cell is not a hole in whichever rule is |
|
# in force. |
|
it "raw html in a cell is escaped, as in prose" |
|
assert_contains "$(md '| <b>x</b> | |
|
|---| |
|
| a |')" "<b>x</b>" |
|
|
|
it "raw html in a cell passes through when the caller asks, as in prose" |
|
assert_contains "$(md_unsafe '| <b>x</b> | |
|
|---| |
|
| a |')" "<b>x</b>" |
|
|
|
it "a table inside a fence is literal" |
|
assert_contains "$(md '``` |
|
| A | |
|
|---| |
|
```')" "| A |" |
|
|
|
suite "markdown: citations" |
|
|
|
it "a citation renders an inline reference" |
|
assert_contains "$(md 'Shown[#smith]. |
|
|
|
[#smith]: John Smith. 2011.')" 'class="citation"' |
|
|
|
it "the citation is numbered" |
|
assert_contains "$(md 'Shown[#smith]. |
|
|
|
[#smith]: John Smith. 2011.')" '(1)' |
|
|
|
it "a locator is carried into the reference" |
|
assert_contains "$(md 'Shown[p. 42][#smith]. |
|
|
|
[#smith]: John Smith. 2011.')" '(p. 42, 1)' |
|
|
|
it "the definition does not render as body text" |
|
assert_not_contains "$(md 'Shown[#smith]. |
|
|
|
[#smith]: John Smith. 2011.')" '<p>[#smith]:' |
|
|
|
it "a citation list is emitted" |
|
assert_contains "$(md 'Shown[#smith]. |
|
|
|
[#smith]: John Smith. 2011.')" 'class="citations"' |
|
|
|
it "the citation body appears in the list" |
|
assert_contains "$(md 'Shown[#smith]. |
|
|
|
[#smith]: John Smith. 2011.')" 'John Smith. 2011.' |
|
|
|
it "markdown inside a citation body is parsed" |
|
assert_contains "$(md 'Shown[#smith]. |
|
|
|
[#smith]: John Smith. *A Book*. 2011.')" '<em>A Book</em>' |
|
|
|
it "the inline reference links to the entry" |
|
assert_contains "$(md 'Shown[#smith]. |
|
|
|
[#smith]: John Smith. 2011.')" 'href="#cn:1"' |
|
|
|
it "the entry links back to the reference" |
|
assert_contains "$(md 'Shown[#smith]. |
|
|
|
[#smith]: John Smith. 2011.')" 'href="#cnref:1"' |
|
|
|
it "numbering follows first use" |
|
out=$(md 'A[#x] and B[#y]. |
|
|
|
[#x]: First. |
|
[#y]: Second.') |
|
assert_contains "$out" '(1)' |
|
|
|
it "the second citation gets the next number" |
|
assert_contains "$out" '(2)' |
|
|
|
it "a repeated citation reuses its number" |
|
out=$(md 'A[#x], B[#y], C[#x]. |
|
|
|
[#x]: First. |
|
[#y]: Second.') |
|
assert_eq "$(printf '%s' "$out" | grep -o 'class="citation">(1)' | wc -l | tr -d ' ')" "2" |
|
|
|
it "footnotes still work alongside citations" |
|
out=$(md 'A[^1] and B[#x]. |
|
|
|
[^1]: a note |
|
[#x]: a citation') |
|
assert_contains "$out" 'fn-item' |
|
|
|
it "the citation still renders too" |
|
assert_contains "$out" 'class="citations"' |
|
|
|
it "a citation inside a fence is literal" |
|
assert_contains "$(md '``` |
|
[#x]: literal |
|
```')" '[#x]: literal' |
|
|
|
suite "markdown: images" |
|
|
|
# Images had no test at all, and the untested path was broken: alt text went |
|
# into the attribute unescaped, so a quote in it closed the attribute early |
|
# and anything after became markup. Found by auditing which emitted elements |
|
# had coverage, not by anything failing. |
|
|
|
it "an image renders with src and alt" |
|
assert_eq "$(md '')" '<p><img src="/img/x.png" alt="alt text" /></p>' |
|
|
|
it "an empty alt is still emitted" |
|
# The attribute is required for the image to be announced as decorative |
|
# rather than left unlabelled. |
|
assert_eq "$(md '')" '<p><img src="/img/x.png" alt="" /></p>' |
|
|
|
it "a title is carried through" |
|
assert_contains "$(md '')" 'title="the title"' |
|
|
|
it "a quote in alt text cannot close the attribute" |
|
assert_contains "$(md '')" 'alt="say "hi""' |
|
|
|
it "a title is escaped as an attribute" |
|
# The title lands inside title="...", so a quote in it must not close the |
|
# attribute. Escaping of the definition-side title is part of the attribute |
|
# safety work; this covers the inline form. |
|
assert_contains "$(md '')" 'title="plain title"' |
|
|
|
it "a quote in the url cannot close the attribute" |
|
assert_contains "$(md '')" 'src="/x.png?q="z""' |
|
|
|
it "a referenced image resolves through its definition" |
|
assert_contains "$(md '![alt][r] |
|
|
|
[r]: /img/ref.png')" '<img src="/img/ref.png" alt="alt" />' |
|
|
|
suite "markdown: horizontal rules" |
|
|
|
it "three dashes make a rule" |
|
assert_contains "$(md 'a |
|
|
|
--- |
|
|
|
b')" '<hr>' |
|
|
|
it "three asterisks make a rule" |
|
assert_contains "$(md 'a |
|
|
|
*** |
|
|
|
b')" '<hr>' |
|
|
|
it "three underscores make a rule" |
|
assert_contains "$(md 'a |
|
|
|
___ |
|
|
|
b')" '<hr>' |
|
|
|
it "a rule separates the paragraphs around it" |
|
out=$(md 'a |
|
|
|
--- |
|
|
|
b') |
|
assert_contains "$out" '<p>a</p>' |
|
assert_contains "$out" '<p>b</p>' |
|
|
|
suite "markdown: ordered lists" |
|
|
|
it "a numbered list becomes an ol" |
|
assert_contains "$(md '1. one |
|
2. two')" '<ol>' |
|
|
|
it "each entry becomes an li" |
|
out=$(md '1. one |
|
2. two') |
|
assert_contains "$out" '<li>' |
|
assert_contains "$out" 'one' |
|
assert_contains "$out" 'two' |
|
|
|
suite "markdown: blockquotes" |
|
|
|
it "a quoted line becomes a blockquote" |
|
assert_contains "$(md '> quoted')" '<blockquote>' |
|
|
|
it "the quoted text is a paragraph inside it" |
|
assert_contains "$(md '> quoted')" 'quoted</p>' |
|
|
|
suite "markdown: autolinks" |
|
|
|
it "an angle-bracketed url becomes a link" |
|
assert_contains "$(md '<https://example.com>')" '<a href="https://example.com">https://example.com</a>' |
|
|
|
it "a quote in an autolink cannot close the attribute" |
|
assert_contains "$(md '<https://x.com/?a="b">')" 'href="https://x.com/?a="b""' |
|
|
|
suite "markdown: html is escaped unless the caller opts in" |
|
|
|
# The parser renders documents it did not write onto pages other people read, |
|
# and it cannot tell an author's deliberate <div> from an injected <script>. |
|
# So escaping is what you get without saying anything, and passthrough is what |
|
# you have to ask for by name -- a caller who forgets the flag is safe, and a |
|
# caller who wants raw HTML has said so somewhere greppable. |
|
|
|
it "a script tag is shown as text, not run" |
|
out=$(md '<script>alert(1)</script>') |
|
assert_contains "$out" '<script>' |
|
assert_not_contains "$out" '<script>' |
|
|
|
it "an inline tag with a handler is shown as text" |
|
out=$(md 'x <img src=y onerror=alert(1)>') |
|
assert_not_contains "$out" '<img' |
|
|
|
it "a block-level tag is shown as text" |
|
out=$(md '<div class="x"> |
|
raw |
|
</div>') |
|
assert_not_contains "$out" '<div class="x">' |
|
|
|
it "the flag brings passthrough back" |
|
assert_contains "$(md_unsafe 'a <b>bold</b>')" '<b>bold</b>' |
|
|
|
it "the flag does not disable attribute escaping" |
|
# The flag is about whole tags the author wrote. A quote breaking out of an |
|
# attribute the parser built is never something a caller asked for. |
|
assert_not_contains "$(md_unsafe 'x[^f" onclick="alert(1)]')" '" onclick="alert(1)"' |
|
|
|
suite "markdown: attributes cannot be broken out of" |
|
|
|
# Each of these put user text into an attribute without escaping the quote, so |
|
# the quote closed the attribute early and what followed became a live handler. |
|
|
|
it "a citation id cannot carry a handler" |
|
out=$(md 'x[#c" onclick="alert(1)] |
|
|
|
[#c" onclick="alert(1)]: A source') |
|
assert_not_contains "$out" '" onclick="alert(1)"' |
|
assert_contains "$out" 'data-citation="c"' |
|
|
|
it "a footnote reference cannot carry a handler" |
|
out=$(md 'x[^f" onclick="alert(1)] |
|
|
|
[^f" onclick="alert(1)]: note') |
|
assert_not_contains "$out" '" onclick="alert(1)"' |
|
|
|
it "a footnote list entry cannot carry a handler" |
|
# The marker and the list entry are built separately; both take the key. |
|
out=$(md 'a[^k" onclick="alert(1)] |
|
|
|
[^k" onclick="alert(1)]: note') |
|
assert_not_contains "$out" 'id="fn-k" onclick=' |
|
|
|
it "a fence language cannot carry a handler" |
|
out=$(md '```x" onload="alert(1) |
|
code |
|
```') |
|
assert_not_contains "$out" '" onload="alert(1)"' |
|
|
|
suite "markdown: urls that execute are dropped" |
|
|
|
it "a javascript: link is emptied" |
|
assert_not_contains "$(md '[x](javascript:alert(1))')" 'href="javascript:' |
|
|
|
it "case does not smuggle it past" |
|
assert_not_contains "$(md '[x](JaVaScRiPt:alert(1))')" 'alert(1)' |
|
|
|
it "leading whitespace does not smuggle it past" |
|
assert_not_contains "$(md '[x]( javascript:alert(1))')" 'alert(1)' |
|
|
|
it "a data: image is emptied" |
|
assert_not_contains "$(md '')" 'src="data:' |
|
|
|
it "vbscript: is dropped too" |
|
assert_not_contains "$(md '[z](vbscript:msgbox(1))')" 'href="vbscript:' |
|
|
|
it "a reference definition cannot hide one" |
|
assert_not_contains "$(md '[a][r] |
|
|
|
[r]: javascript:alert(1)')" 'href="javascript:' |
|
|
|
it "an ordinary link is untouched" |
|
assert_contains "$(md '[ok](https://example.com)')" 'href="https://example.com"' |
|
|
|
it "a relative link is untouched" |
|
assert_contains "$(md '[ok](/local/page)')" 'href="/local/page"' |
|
|
|
suite "markdown: percent-encoded schemes are decoded before the check" |
|
|
|
# A browser percent-decodes an href before it decides what scheme the URL |
|
# holds, so a check that reads the raw text reads something the browser never |
|
# sees. Every one of these was emitted verbatim and ran on click. |
|
|
|
it "a fully encoded scheme is caught" |
|
assert_not_contains "$(md '[x](%6a%61%76%61%73%63%72%69%70%74:alert(1))')" '%6a%61%76%61' |
|
|
|
it "a partly encoded scheme is caught" |
|
assert_not_contains "$(md '[x](%6aavascript:alert(1))')" '%6aavascript' |
|
|
|
it "a double-encoded scheme is caught" |
|
# %256a is a percent sign that is itself encoded: one pass leaves %6a, so the |
|
# decode has to run until the string stops changing. |
|
assert_not_contains "$(md '[x](%256a%61%76%61%73%63%72%69%70%74:alert(1))')" 'alert(1)' |
|
|
|
it "an encoded data: image is caught" |
|
assert_not_contains "$(md '')" '%64%61%74%61' |
|
|
|
it "a reference definition cannot hide an encoded scheme" |
|
assert_not_contains "$(md '[a][r] |
|
|
|
[r]: %6a%61%76%61%73%63%72%69%70%74:alert(1)')" 'alert(1)' |
|
|
|
it "a legitimate percent escape survives into the output" |
|
# The decode is done on a throwaway copy read only by the scheme test. The URL |
|
# that is emitted is the one the author wrote, so a space encoded in a path is |
|
# still encoded when it gets to the browser. |
|
assert_contains "$(md '[ok](https://example.com/a%20b)')" 'href="https://example.com/a%20b"' |
|
|
|
it "a percent escape in an image path survives too" |
|
assert_contains "$(md '')" 'src="/i/a%20b.png"' |
|
|
|
it "the earlier vectors are still blocked" |
|
assert_not_contains "$(md '[x](javascript:alert(1))')" 'href="javascript:' |
|
assert_not_contains "$(md '[x](JaVaScRiPt:alert(1))')" 'alert(1)' |
|
assert_not_contains "$(md '[x]( javascript:alert(1))')" 'alert(1)' |
|
|
|
suite "markdown: decoding reveals letters that must be folded" |
|
|
|
# The probe was lowercased once, before decoding. Decoding then produced |
|
# letters that had never been folded -- "%4a" is lowercase already and decodes |
|
# to "J" -- so "%4Aavascript:" arrived at the check as "Javascript:" and did |
|
# not match a pattern written in lowercase. A browser reads a scheme |
|
# case-insensitively, so it ran. The fold has to follow the decode that |
|
# produced the letter, on every pass. |
|
|
|
it "an uppercase escape at the start is caught" |
|
assert_not_contains "$(md '[x](%4Aavascript:alert(1))')" '%4Aavascript' |
|
|
|
it "a mixed-case pair of escapes is caught" |
|
assert_not_contains "$(md '[x](%4a%61vascript:alert(1))')" 'alert(1)' |
|
|
|
it "an uppercase escape in the middle is caught" |
|
assert_not_contains "$(md '[x](jav%41script:alert(1))')" 'jav%41script' |
|
|
|
it "an uppercase escape in another scheme is caught" |
|
assert_not_contains "$(md '[x](%56%42script:alert(1))')" 'alert(1)' |
|
|
|
it "uppercase hex in a real path is untouched" |
|
# The decoder reads either case now, but it reads a throwaway copy. What is |
|
# emitted is what the author wrote, so an uppercase escape in a genuine path |
|
# survives byte for byte. |
|
assert_contains "$(md '[ok](https://example.com/A%2FB)')" 'href="https://example.com/A%2FB"' |
|
|
|
it "a utf-8 sequence in a path is untouched" |
|
assert_contains "$(md '[ok](https://example.com/%E2%9C%93)')" 'href="https://example.com/%E2%9C%93"' |
|
|
|
it "the decoder reads an uppercase pair on its own terms" |
|
# sanitize_url folds before decoding, so the decoder is never handed |
|
# uppercase hex in practice and narrowing this class back passes every other |
|
# test here. It is still wrong: a decoder that reads %6a and not %6A is |
|
# incorrect independently of what its one caller happens to do, and the next |
|
# caller may not fold first. Exercised directly for that reason. |
|
out=$(printf '%%4Aavascript\n' | "$AWK" -f "$REPO_ROOT/markdown.awk" -e 'BEGIN{ print pct_decode("%4Aavascript") }' 2>/dev/null </dev/null) |
|
if [ "$out" = "Javascript" ]; then pass; else fail "decoder gave [$out], wanted [Javascript]"; fi |
|
summary |
|
|