-
-
Notifications
You must be signed in to change notification settings - Fork 32.5k
gh-135661: Fix CDATA section parsing in HTMLParser #135665
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
"] ]>" and "]] >" no longer end the CDATA section.
Lib/html/parser.py
Outdated
j = rawdata.find(']]>') | ||
if j < 0: | ||
return -1 | ||
self.unknown_decl(rawdata[i+3: j]) | ||
return j + 3 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the HTML5 standard (https://html.spec.whatwg.org/multipage/parsing.html#markup-declaration-open-state), it should be either data or bogus comment (which ends with >
, not ]]>
), but this depends on the context. It may be that I incorrectly understand the HTML5 standard, because this part is difficult to implement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried copying the content of the tests in the following file:
<!DOCTYPE html>
<html>
<body>
<![CDATA[just some plain text]]><hr>
<![CDATA[<!-- not a comment -->]]><hr>
<![CDATA[¬-an-entity-ref;]]><hr>
<![CDATA[<not a='start tag'>]]><hr>
<![CDATA[]]><hr>
<![CDATA[[[I have many brackets]]]]><hr>
<![CDATA[I have a > in the middle]]><hr>
<![CDATA[I have a ]] in the middle]]><hr>
<![CDATA[] ]>]]><hr>
<![CDATA[]] >]]><hr>
<![CDATA[
if (a < b && a > b) {
printf("[<marquee>How?</marquee>]");
}
]]><hr>
</body>
</html>
and this was the result on Firefox:
<html><head></head><body>
<!--[CDATA[just some plain text]]--><hr>
<!--[CDATA[<!-- not a comment ---->]]><hr>
<!--[CDATA[¬-an-entity-ref;]]--><hr>
<!--[CDATA[<not a='start tag'-->]]><hr>
<!--[CDATA[]]--><hr>
<!--[CDATA[[[I have many brackets]]]]--><hr>
<!--[CDATA[I have a --> in the middle]]><hr>
<!--[CDATA[I have a ]] in the middle]]--><hr>
<!--[CDATA[] ]-->]]><hr>
<!--[CDATA[]] -->]]><hr>
<!--[CDATA[
if (a < b && a --> b) {
printf("[<marquee>How?</marquee>]");
}
]]><hr>
</body></html>

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, and if you try <svg><text y="100"><![CDATA[foo<br>bar]]></text></svg>
, you will see that content between <![CDATA[
and ]]>
is interpreted as a raw data.
This is context dependent.
HTMLParser is actually just a tokenizer. To determine the context automatically, it needs to support the stack of open elements and to know what elements are in the HTML namespace. This is all in the specification, and we will implement this in future. But this is a different level of complexity. So I solved the issue by letting the user to determine the context. New method support_cdata()
sets how HTMLParser will parse CDATA. This is not good, but perhaps better than the current state.
Lib/html/parser.py
Outdated
j = rawdata.find(']]>') | ||
if j < 0: | ||
return -1 | ||
self.unknown_decl(rawdata[i+3: j]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self.unknown_decl(rawdata[i+3: j]) | |
self.unknown_decl(rawdata[i+3:j]) |
Lib/html/parser.py
Outdated
j = rawdata.find(']]>') | ||
if j < 0: | ||
return -1 | ||
self.unknown_decl(rawdata[i+3: j]) | ||
return j + 3 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried copying the content of the tests in the following file:
<!DOCTYPE html>
<html>
<body>
<![CDATA[just some plain text]]><hr>
<![CDATA[<!-- not a comment -->]]><hr>
<![CDATA[¬-an-entity-ref;]]><hr>
<![CDATA[<not a='start tag'>]]><hr>
<![CDATA[]]><hr>
<![CDATA[[[I have many brackets]]]]><hr>
<![CDATA[I have a > in the middle]]><hr>
<![CDATA[I have a ]] in the middle]]><hr>
<![CDATA[] ]>]]><hr>
<![CDATA[]] >]]><hr>
<![CDATA[
if (a < b && a > b) {
printf("[<marquee>How?</marquee>]");
}
]]><hr>
</body>
</html>
and this was the result on Firefox:
<html><head></head><body>
<!--[CDATA[just some plain text]]--><hr>
<!--[CDATA[<!-- not a comment ---->]]><hr>
<!--[CDATA[¬-an-entity-ref;]]--><hr>
<!--[CDATA[<not a='start tag'-->]]><hr>
<!--[CDATA[]]--><hr>
<!--[CDATA[[[I have many brackets]]]]--><hr>
<!--[CDATA[I have a --> in the middle]]><hr>
<!--[CDATA[I have a ]] in the middle]]--><hr>
<!--[CDATA[] ]-->]]><hr>
<!--[CDATA[]] -->]]><hr>
<!--[CDATA[
if (a < b && a --> b) {
printf("[<marquee>How?</marquee>]");
}
]]><hr>
</body></html>

* Add HTMLParser.support_cdata().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand HTML enough to judge if this change is the right one.
I'm adding docs/changelog suggestions to clarify the behaviour, as I understand it.
If *flag* is false, then the :meth:`handle_comment` method will be called | ||
for ``<![CDATA[...>``. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should mention the default behaviour.
If *flag* is false, then the :meth:`handle_comment` method will be called | |
for ``<![CDATA[...>``. | |
If *flag* is false, or if :meth:`!support_cdata` has not been called yet, | |
then the :meth:`handle_comment` method will be called for ``<![CDATA[...>``. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is actually a weak point of such approach. It should be true by default to be able to parse valid HTML (when <![CDATA[...]]>
is only used in foreign content) by default. But secure parsing needs to set it to false at the beginning and the set to true or false after every open or close tag, depending on the complex algorithm.
So we should set it to true by default if we keep this approach.
If *flag* is false, then the :meth:`handle_comment` method will be called | ||
for ``<![CDATA[...>``. | ||
|
||
.. versionadded:: 3.13.6 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should mention the previous behaviour.
.. versionadded:: 3.13.6 | |
.. versionadded:: 3.13.6 | |
Previously, :meth:`unknown_decl` was called for ``<![CDATA[...>``. |
Fix CDATA section parsing in :class:`html.parser.HTMLParser` according to | ||
the HTML5 standard: ``] ]>`` and ``]] >`` no longer end the CDATA section. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix CDATA section parsing in :class:`html.parser.HTMLParser` according to | |
the HTML5 standard: ``] ]>`` and ``]] >`` no longer end the CDATA section. | |
Fix CDATA section parsing in :class:`html.parser.HTMLParser` according to | |
the HTML5 standard: ``] ]>`` and ``]] >`` no longer end the CDATA section. | |
By default, :meth:`~HTMLParser.handle_comment` is called for CDATA. | |
The old behavior (calling :meth:`~HTMLParser.unknown_decl`) can be restored | |
using a new method, :meth:`~HTMLParser.support_cdata`. |
def support_cdata(self, flag=True): | ||
self._support_cdata = flag |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not convinced this is the best way to handle the issue.
Since this solves a security issue, it also needs to be added to a bug fix release and backported to several other releases. Making the method private should be enough to solve the security issue in the short term without adding to the public API. This will also give us time to think about alternative (and possibly better) solution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Private means that users shouldn't call it. Given that nothing in html
calls it, making it private is the same as not adding it at all.
It looks like the issue can't be solved in CPython alone -- users need to adapt their code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be solved in CPython, and I hope we will, but it looks like the solution will be too complex to risk backporting it to security-only branches. Providing a method to alter behavior we pass the ball to user side. This is not good, but otherwise the problem will left unsolved. And without closing this hole, all other security fixes for HTMLParser are worthless.
I will try other approach, but can't guarantee anything.
Here's an idea (though it might be naive): what if the parser called a |
"] ]>" and "]] >" no longer end the CDATA section.