-
-
Notifications
You must be signed in to change notification settings - Fork 30.9k
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
HTMLParser: comma in attribute values with/without space #85914
Comments
HTML tags that have a attribute name starting with a comma character aren't parsed and break future calls to feed(). The problem occurs when such attribute is the second one or later in the HTML tag. Doesn't seems to affect when it's the first attribute. #POC: from html.parser import HTMLParser
class MyHTMLParser(HTMLParser):
def handle_starttag(self, tag, attrs):
print("Encountered a start tag:", tag)
parser = MyHTMLParser()
#This is ok
parser.feed('<yyy id="poc" a,="">')
#This breaks
parser.feed('<zzz id="poc" ,a="">')
#Future calls to feed() will not work
parser.feed('<img id="poc" src=x>') |
HTML 5.2 specification says "Attribute names must consist of one or more characters other than the space characters, U+0000 NULL, U+0022 QUOTATION MARK ("), U+0027 APOSTROPHE ('), U+003E GREATER-THAN SIGN (>), U+002F SOLIDUS (/), and U+003D EQUALS SIGN (=) characters, the control characters, and any characters that are not defined by Unicode." It is confirmed in the "12.2 Parsing HTML documents" section of the "HTML Living Standard": Consume the next input character: U+0009 CHARACTER TABULATION (tab) I understand that "," *is* a legit character in a HTML attribute name. So "a," and ",a" *are* valid HTML attribute names. Do I understand correctly? |
Yes, I understand that in the same way. Both are valid attr names. Maybe it's worth noting that Javascript has no problem handling this. |
HTMLParser.check_for_whole_start_tag() uses locatestarttagend_tolerant regular expression to find the end of the start tag. This regex cuts the string at the first comma (","), but not if the comma is the first character of an attribute name
The regex is quite complex: locatestarttagend_tolerant = re.compile(r"""
<[a-zA-Z][^\t\n\r\f />\x00]* # tag name
(?:[\s/]* # optional whitespace before attribute name
(?:(?<=['"\s/])[^\s/>][^\s/=>]* # attribute name
(?:\s*=+\s* # value indicator
(?:'[^']*' # LITA-enclosed value
|"[^"]*" # LIT-enclosed value
|(?!['"])[^>\s]* # bare value
)
(?:\s*,)* # possibly followed by a comma
)?(?:\s|/(?!>))*
)*
)?
\s* # trailing whitespace
""", re.VERBOSE)
endendtag = re.compile('>') The problem is that this part of the regex: #(?:\s*,)* # possibly followed by a comma The comma is not seen as part of the attribute name. |
Also, there is no warning about security in the html.parser documentation? Is this module mature and maintained enough to be considered as reliable? Or should we warn users about possible issues on corner cases, and point to BeautilfulSoup for a more mature HTML parser? |
The html.parser follows the HTML 5 specs as closely as possible. There are a few corner cases where it behaves slightly differently but it's only while dealing with invalid markup, and the differences should be trivial and generally not worth the extra complexity to deal with them. In this case, if I recall correctly, the way the comma is handled is just a left-over from the previous version of the parser, that predates the HTML 5 specs. In tags like <tag foo=bar,baz=asd> there was an ambiguous situation and parsing it <tag foo="bar" baz="asd"> was deemed a reasonable interpretation, so the comma was treated as an attribute separator (and there should be test cases for this). This likely caused the issue reported by the OP, and I think it should be fixed, even if technically it's a change in behavior and will break some of the tests. If I'm reading the specs[0] correctly:
I'm not aware of any specific security issues, since html.parser just implements the parser described by the HTML 5 specs. If there are any security issues caused by divergences from the specs, they should be fixed. I'm not sure why a warning would be needed.
Even though it hasn't been updated to the latest version of the specs (5.2 at the time of writing), it has been updated to implement the parsing rules described by the HTML 5 specs. I don't know if the parsing rules changed between 5.0 and 5.2.
BeautifulSoup is built on top of html.parser (and can also use other parses, like lxml). BS uses the underlying parsers to parse the HTML, then builds the tree and provides, among other things, functions to search and edit it. [0] starting from https://www.w3.org/TR/html52/syntax.html#tokenizer-before-attribute-name-state |
Ezio, TL,DR: Testing in browsers and adding two tests for this issue. A: comma without spaces Tests for browsers: Serializations:
Same serialization in these 3 rendering engines Adding: def test_comma_between_unquoted_attributes(self):
# bpo 41748
self._run_check('<div class=bar,baz=asd>',
[('starttag', 'div', [('class', 'bar,baz=asd')])]) ❯ ./python.exe -m test -v test_htmlparser … Ran 47 tests in 0.168s OK == Tests result: SUCCESS == 1 test OK. Total duration: 369 ms So this is working as expected for the first test. B: comma with spaces Tests for browsers: Serializations:
Same serialization in these 3 rendering engines Adding ❯ ./python.exe -m test -v test_htmlparser This is failing. ====================================================================== Traceback (most recent call last):
File "/Users/karl/code/cpython/Lib/test/test_htmlparser.py", line 493, in test_comma_with_space_between_unquoted_attributes
self._run_check('<div class=bar ,baz=asd>',
File "/Users/karl/code/cpython/Lib/test/test_htmlparser.py", line 95, in _run_check
self.fail("received events did not match expected events" +
AssertionError: received events did not match expected events
Source:
'<div class=bar ,baz=asd>'
Expected:
[('starttag', 'div', [('class', 'bar'), (',baz', 'asd')])]
Received:
[('data', '<div class=bar ,baz=asd>')] I started to look into the code of parser.py which I'm not familiar (yet) with. Lines 42 to 52 in 6329893
Do you have a suggestion to fix it? |
Ah! This is fixing it diff --git a/Lib/html/parser.py b/Lib/html/parser.py
index 6083077981..ffff790666 100644
--- a/Lib/html/parser.py
+++ b/Lib/html/parser.py
@@ -44,7 +44,7 @@
(?:\s*=+\s* # value indicator
(?:'[^']*' # LITA-enclosed value
|"[^"]*" # LIT-enclosed value
- |(?!['"])[^>\s]* # bare value
+ |(?!['"])[^>]* # bare value
)
(?:\s*,)* # possibly followed by a comma
)?(?:\s|/(?!>))* Ran 48 tests in 0.175s OK == Tests result: SUCCESS == |
Writing tests that verify the expected behavior is a great first step. The expected output in the tests should match the behavior described by the HTML 5 specs (which should also correspond to the browsers' behavior), and should initially fail. You can start creating a PR with only the tests, clarifying that it's a work in progress, or wait until you have the fix too. The next step would be tweaking the regex and the code until both the new tests and all the other ones work (excluding the one with the commas you are fixing). You can then commit the fix in the same branch and push it -- GitHub will automatically update the PR.
If you are familiar enough with regexes, you could try to figure out whether it matches the invalid attributes or not, and if not why (I took a quick look and I didn't see anything immediately wrong in the regexes). Since the output of the failing test is [('data', '<div class=bar ,baz=asd>')], it's likely that the parser doesn't know how to handle it and passes it to one of the handle_data() in the goahead() method. You can figure out which one is being called and see which are the if-conditions that are leading the interpreter down this path rather than the usual path where the attributes are parsed correctly. If you have other questions let me know :) |
Status: The PR should be ready and completed |
Oh, thank you Karl Dubost for the fix! |
Merged! Thanks for the report and the PR! |
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields:
The text was updated successfully, but these errors were encountered: