Skip to content

Commit ba25359

Browse files
committed
Reject all browser-recognized comment-closing sequences in Comment::set_text.
Previously `Comment::set_text` only rejected the canonical `-->` terminator, but a WHATWG-conformant browser also closes a comment on three more byte sequences: 1. `--!>` anywhere in the body (comment-end-bang state). 2. A leading `>` (abrupt-closing-of-empty-comment from comment-start). 3. A leading `->` (abrupt-closing-of-empty-comment from comment-start-dash).
1 parent b00626a commit ba25359

2 files changed

Lines changed: 50 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
- Renamed the internal-use feature `integration_test` to `_integration_test`. The leading
3131
underscore signals to `cargo-semver-checks` and similar tools that the feature is not
3232
part of the public API.
33+
- `Comment::set_text` now also rejects `--!>`, a leading `>`, and a leading `->`, which
34+
WHATWG-conformant browsers treat as comment terminators. Previously only `-->` was
35+
rejected, so a caller passing attacker-influenced data could let an attacker break out
36+
of the comment and inject HTML (security fix).
3337

3438
## v2.9.0
3539

src/rewritable_units/tokens/comment.rs

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ use thiserror::Error;
1212
/// An error that occurs when invalid value is provided for the HTML comment text.
1313
#[derive(Error, Debug, Eq, PartialEq, Copy, Clone)]
1414
pub enum CommentTextError {
15-
/// The provided value contains the `-->` character sequence that preemptively closes the comment.
16-
#[error("Comment text shouldn't contain comment closing sequence (`-->`).")]
15+
/// The provided value contains a byte sequence that a WHATWG-conformant browser
16+
/// treats as a comment terminator (`-->`, `--!>`, a leading `>`, or a leading `->`).
17+
#[error("Comment text shouldn't contain a comment-closing sequence.")]
1718
CommentClosingSequence,
1819

1920
/// The provided value contains a character that can't be represented in the document's [`encoding`].
@@ -36,6 +37,20 @@ pub struct Comment<'i> {
3637
user_data: Box<dyn Any>,
3738
}
3839

40+
/// Returns `true` if `text`, when emitted between `<!--` and `-->`, would let a
41+
/// WHATWG-conformant browser close the comment earlier than the trailing `-->`
42+
/// marker, leaking whatever follows as live markup.
43+
///
44+
/// Four sequences trigger this:
45+
///
46+
/// 1. `-->` anywhere in the body (comment-end state).
47+
/// 2. `--!>` anywhere in the body (comment-end-bang state).
48+
/// 3. A leading `>` (abrupt-closing-of-empty-comment from comment-start).
49+
/// 4. A leading `->` (abrupt-closing-of-empty-comment from comment-start-dash).
50+
fn contains_comment_closing_sequence(text: &str) -> bool {
51+
text.contains("-->") || text.contains("--!>") || text.starts_with('>') || text.starts_with("->")
52+
}
53+
3954
impl<'i> Comment<'i> {
4055
#[inline]
4156
#[must_use]
@@ -68,22 +83,22 @@ impl<'i> Comment<'i> {
6883
/// Sets the text of the comment.
6984
#[inline]
7085
pub fn set_text(&mut self, text: &str) -> Result<(), CommentTextError> {
71-
if text.contains("-->") {
72-
Err(CommentTextError::CommentClosingSequence)
73-
} else {
74-
// NOTE: if character can't be represented in the given
75-
// encoding then encoding_rs replaces it with a numeric
76-
// character reference. Character references are not
77-
// supported in comments, so we need to bail.
78-
match BytesCow::owned_from_str_without_replacements(text, self.encoding) {
79-
Ok(text) => {
80-
self.text = text;
81-
self.raw.set_modified();
82-
83-
Ok(())
84-
}
85-
Err(_) => Err(CommentTextError::UnencodableCharacter),
86+
if contains_comment_closing_sequence(text) {
87+
return Err(CommentTextError::CommentClosingSequence);
88+
}
89+
90+
// NOTE: if character can't be represented in the given
91+
// encoding then encoding_rs replaces it with a numeric
92+
// character reference. Character references are not
93+
// supported in comments, so we need to bail.
94+
match BytesCow::owned_from_str_without_replacements(text, self.encoding) {
95+
Ok(text) => {
96+
self.text = text;
97+
self.raw.set_modified();
98+
99+
Ok(())
86100
}
101+
Err(_) => Err(CommentTextError::UnencodableCharacter),
87102
}
88103
}
89104

@@ -314,11 +329,20 @@ mod tests {
314329

315330
#[test]
316331
fn comment_closing_sequence_in_text() {
317-
rewrite_comment(b"<!-- foo -->", UTF_8, |c| {
318-
let err = c.set_text("foo -- bar --> baz").unwrap_err();
319-
320-
assert_eq!(err, CommentTextError::CommentClosingSequence);
321-
});
332+
// Every byte sequence here is treated as a comment terminator by a
333+
// WHATWG-conformant browser when emitted between `<!--` and `-->`.
334+
// See `contains_comment_closing_sequence`.
335+
for payload in ["foo -- bar --> baz", "foo --!> bar", ">foo", "->foo"] {
336+
rewrite_comment(b"<!-- foo -->", UTF_8, |c| {
337+
let err = c.set_text(payload).unwrap_err();
338+
339+
assert_eq!(
340+
err,
341+
CommentTextError::CommentClosingSequence,
342+
"expected `{payload}` to be rejected",
343+
);
344+
});
345+
}
322346
}
323347

324348
#[test]

0 commit comments

Comments
 (0)