|
23 | 23 | * Key Djot requirements handled: |
24 | 24 | * - Blank lines required around block elements (headings, code blocks, lists) |
25 | 25 | * - Nested lists require blank line before the nested portion |
| 26 | + * |
| 27 | + * SECURITY: this converter is NOT a sanitizer. Its output is Djot markup that |
| 28 | + * may still contain content derived from the input; render untrusted input with |
| 29 | + * safe mode enabled on the downstream renderer. By default the converter IGNORES |
| 30 | + * the `data-djot-src` / `data-djot-raw` round-trip attributes on the input (they |
| 31 | + * would otherwise be re-emitted verbatim as raw Djot, letting a crafted attribute |
| 32 | + * smuggle a raw-HTML block -> live <script> into the output). Only enable |
| 33 | + * round-trip extraction via the constructor flag when the HTML is TRUSTED (e.g. |
| 34 | + * produced by djot itself for a faithful round-trip). |
26 | 35 | */ |
27 | 36 | class HtmlToDjot |
28 | 37 | { |
| 38 | + /** |
| 39 | + * When true, trust and re-emit the `data-djot-src` / `data-djot-raw` |
| 40 | + * round-trip attributes on the input. Default false: untrusted HTML must not |
| 41 | + * be able to smuggle raw Djot (incl. a raw-HTML block) through them. |
| 42 | + */ |
| 43 | + protected bool $trustedRoundTrip = false; |
| 44 | + |
| 45 | + public function __construct(bool $trustedRoundTrip = false) |
| 46 | + { |
| 47 | + $this->trustedRoundTrip = $trustedRoundTrip; |
| 48 | + } |
| 49 | + |
29 | 50 | protected int $listDepth = 0; |
30 | 51 |
|
31 | 52 | protected bool $inPre = false; |
@@ -948,6 +969,14 @@ protected function processPreBlock(DOMElement $node): string |
948 | 969 |
|
949 | 970 | protected function extractRoundTripSource(DOMElement $node, string $tagName): ?string |
950 | 971 | { |
| 972 | + // Untrusted HTML must not be able to smuggle raw Djot through a |
| 973 | + // `data-djot-src` attribute (it is emitted verbatim, so a crafted value |
| 974 | + // could inject a raw-HTML block -> live <script>). Only honor it when the |
| 975 | + // caller explicitly trusts the source. |
| 976 | + if (!$this->trustedRoundTrip) { |
| 977 | + return null; |
| 978 | + } |
| 979 | + |
951 | 980 | if (!$node->hasAttribute('data-djot-src')) { |
952 | 981 | return null; |
953 | 982 | } |
@@ -1852,8 +1881,12 @@ protected function processSpan(DOMElement $node): string |
1852 | 1881 | return '\\' . $node->textContent; |
1853 | 1882 | } |
1854 | 1883 |
|
1855 | | - // Check for raw inline content (round-trip support) |
1856 | | - if ($node->hasAttribute('data-djot-raw')) { |
| 1884 | + // Check for raw inline content (round-trip support). Only honor it for |
| 1885 | + // trusted input: `data-djot-raw` is re-emitted verbatim as a raw inline |
| 1886 | + // span (`` `...`{=html} ``), so a crafted attribute on untrusted HTML |
| 1887 | + // could smuggle live markup. Untrusted input falls through to ordinary |
| 1888 | + // span processing (getElementAttributes drops the data-djot-* attribute). |
| 1889 | + if ($this->trustedRoundTrip && $node->hasAttribute('data-djot-raw')) { |
1857 | 1890 | return $this->processRawInline($node); |
1858 | 1891 | } |
1859 | 1892 |
|
@@ -2224,7 +2257,10 @@ protected function formatCaptionText(string $captionText): string |
2224 | 2257 |
|
2225 | 2258 | protected function convertInlineFragmentToDjot(string $html): string |
2226 | 2259 | { |
2227 | | - $converter = new self(); |
| 2260 | + // Propagate the trust setting so a trusted round-trip parent keeps |
| 2261 | + // honoring inner round-trip attributes in the recursive sub-conversion |
| 2262 | + // (and an untrusted parent keeps ignoring them). |
| 2263 | + $converter = new self($this->trustedRoundTrip); |
2228 | 2264 | $converter->preserveTextWhitespace = true; |
2229 | 2265 |
|
2230 | 2266 | $doc = new DOMDocument(); |
|
0 commit comments