Skip to content

Commit fcd0258

Browse files
committed
fix(security): Passing unescaped strings in attribute values
Introduced in #184. Previously, escaped values became unescaped on the serialization step.
1 parent 0012eb7 commit fcd0258

File tree

5 files changed

+40
-134
lines changed

5 files changed

+40
-134
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010

1111
- Replace `remove_style_tags` with `keep_style_tags`.
1212

13+
### Fixed
14+
15+
- **SECURITY**: Passing unescaped strings in attribute values introduced in [#184](https://github.com/Stranger6667/css-inline/issues/184).
16+
Previously, escaped values became unescaped on the serialization step.
17+
1318
### Removed
1419

1520
- The `inline_style_tags` configuration option.

bindings/python/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010

1111
- Replace `remove_style_tags` with `keep_style_tags`.
1212

13+
### Fixed
14+
15+
- **SECURITY**: Passing unescaped strings in attribute values introduced in [#184](https://github.com/Stranger6667/css-inline/issues/184).
16+
Previously, escaped values became unescaped on the serialization step.
17+
1318
### Removed
1419

1520
- The `inline_style_tags` configuration option.

bindings/wasm/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010

1111
- Replace `remove_style_tags` with `keep_style_tags`.
1212

13+
### Fixed
14+
15+
- **SECURITY**: Passing unescaped strings in attribute values introduced in [#184](https://github.com/Stranger6667/css-inline/issues/184).
16+
Previously, escaped values became unescaped on the serialization step.
17+
1318
### Removed
1419

1520
- The `inline_style_tags` configuration option.

css-inline/src/html/serializer.rs

Lines changed: 24 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use super::{
2-
attributes::Attributes,
32
document::Document,
43
node::{ElementData, NodeData, NodeId},
54
};
6-
use html5ever::{local_name, namespace_url, ns, QualName};
5+
use html5ever::{
6+
local_name,
7+
serialize::{serialize, Serialize, SerializeOpts, Serializer, TraversalScope},
8+
};
79
use std::{io, io::Write};
810

911
pub(crate) fn serialize_to<W: Write>(
@@ -18,8 +20,7 @@ pub(crate) fn serialize_to<W: Write>(
1820
keep_style_tags,
1921
keep_link_tags,
2022
);
21-
let mut serializer = HtmlSerializer::new(writer);
22-
sink.serialize(&mut serializer)
23+
serialize(writer, &sink, SerializeOpts::default())
2324
}
2425

2526
/// Intermediary structure for serializing an HTML document.
@@ -69,23 +70,37 @@ impl<'a> Sink<'a> {
6970
false
7071
}
7172
}
72-
fn serialize_children<W: Write>(&self, serializer: &mut HtmlSerializer<W>) -> io::Result<()> {
73+
fn serialize_children<S: Serializer>(&self, serializer: &mut S) -> io::Result<()> {
7374
for child in self.document.children(self.node) {
74-
self.for_node(child).serialize(serializer)?;
75+
self.for_node(child)
76+
.serialize(serializer, TraversalScope::IncludeNode)?;
7577
}
7678
Ok(())
7779
}
78-
fn serialize<W: Write>(&self, serializer: &mut HtmlSerializer<W>) -> io::Result<()> {
80+
}
81+
82+
impl<'a> Serialize for Sink<'a> {
83+
fn serialize<S>(&self, serializer: &mut S, _: TraversalScope) -> io::Result<()>
84+
where
85+
S: Serializer,
86+
{
7987
match self.data() {
8088
NodeData::Element { element, .. } => {
8189
if self.should_skip_element(element) {
8290
return Ok(());
8391
}
84-
serializer.start_elem(&element.name, &element.attributes)?;
92+
serializer.start_elem(
93+
element.name.clone(),
94+
element
95+
.attributes
96+
.map
97+
.iter()
98+
.map(|(key, value)| (key, &**value)),
99+
)?;
85100

86101
self.serialize_children(serializer)?;
87102

88-
serializer.end_elem(&element.name)?;
103+
serializer.end_elem(element.name.clone())?;
89104
Ok(())
90105
}
91106
NodeData::Document => self.serialize_children(serializer),
@@ -99,130 +114,6 @@ impl<'a> Sink<'a> {
99114
}
100115
}
101116

102-
struct ElemInfo {
103-
ignore_children: bool,
104-
}
105-
106-
struct HtmlSerializer<Wr: Write> {
107-
writer: Wr,
108-
stack: Vec<ElemInfo>,
109-
}
110-
111-
impl<Wr: Write> HtmlSerializer<Wr> {
112-
fn new(writer: Wr) -> Self {
113-
HtmlSerializer {
114-
writer,
115-
stack: vec![ElemInfo {
116-
ignore_children: false,
117-
}],
118-
}
119-
}
120-
121-
fn parent(&mut self) -> &mut ElemInfo {
122-
self.stack.last_mut().expect("Stack is empty")
123-
}
124-
125-
fn start_elem(&mut self, name: &QualName, attrs: &Attributes) -> io::Result<()> {
126-
if self.parent().ignore_children {
127-
self.stack.push(ElemInfo {
128-
ignore_children: true,
129-
});
130-
return Ok(());
131-
}
132-
133-
self.writer.write_all(b"<")?;
134-
self.writer.write_all(name.local.as_bytes())?;
135-
for (name, value) in &attrs.map {
136-
self.writer.write_all(b" ")?;
137-
138-
match name.ns {
139-
ns!() => (),
140-
ns!(xml) => self.writer.write_all(b"xml:")?,
141-
ns!(xmlns) => {
142-
if name.local != local_name!("xmlns") {
143-
self.writer.write_all(b"xmlns:")?;
144-
}
145-
}
146-
ns!(xlink) => self.writer.write_all(b"xlink:")?,
147-
_ => {
148-
self.writer.write_all(b"unknown_namespace:")?;
149-
}
150-
}
151-
152-
self.writer.write_all(name.local.as_bytes())?;
153-
self.writer.write_all(b"=\"")?;
154-
self.writer.write_all(value.as_bytes())?;
155-
self.writer.write_all(b"\"")?;
156-
}
157-
self.writer.write_all(b">")?;
158-
159-
let ignore_children = name.ns == ns!(html)
160-
&& matches!(
161-
name.local,
162-
local_name!("area")
163-
| local_name!("base")
164-
| local_name!("basefont")
165-
| local_name!("bgsound")
166-
| local_name!("br")
167-
| local_name!("col")
168-
| local_name!("embed")
169-
| local_name!("frame")
170-
| local_name!("hr")
171-
| local_name!("img")
172-
| local_name!("input")
173-
| local_name!("keygen")
174-
| local_name!("link")
175-
| local_name!("meta")
176-
| local_name!("param")
177-
| local_name!("source")
178-
| local_name!("track")
179-
| local_name!("wbr")
180-
);
181-
182-
self.stack.push(ElemInfo { ignore_children });
183-
184-
Ok(())
185-
}
186-
187-
fn end_elem(&mut self, name: &QualName) -> io::Result<()> {
188-
let info = match self.stack.pop() {
189-
Some(info) => info,
190-
_ => panic!("no ElemInfo"),
191-
};
192-
if info.ignore_children {
193-
return Ok(());
194-
}
195-
196-
self.writer.write_all(b"</")?;
197-
self.writer.write_all(name.local.as_bytes())?;
198-
self.writer.write_all(b">")
199-
}
200-
201-
fn write_text(&mut self, text: &str) -> io::Result<()> {
202-
self.writer.write_all(text.as_bytes())
203-
}
204-
205-
fn write_comment(&mut self, text: &str) -> io::Result<()> {
206-
self.writer.write_all(b"<!--")?;
207-
self.writer.write_all(text.as_bytes())?;
208-
self.writer.write_all(b"-->")
209-
}
210-
211-
fn write_doctype(&mut self, name: &str) -> io::Result<()> {
212-
self.writer.write_all(b"<!DOCTYPE ")?;
213-
self.writer.write_all(name.as_bytes())?;
214-
self.writer.write_all(b">")
215-
}
216-
217-
fn write_processing_instruction(&mut self, target: &str, data: &str) -> io::Result<()> {
218-
self.writer.write_all(b"<?")?;
219-
self.writer.write_all(target.as_bytes())?;
220-
self.writer.write_all(b" ")?;
221-
self.writer.write_all(data.as_bytes())?;
222-
self.writer.write_all(b">")
223-
}
224-
}
225-
226117
#[cfg(test)]
227118
mod tests {
228119
use super::Document;

css-inline/tests/test_inlining.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ fn href_attribute_unchanged() {
245245
</head>
246246
<body>
247247
<h1 style="color:blue;">Big Text</h1>
248-
<a href="https://example.org/test?a=b&c=d">Link</a>
248+
<a href="https://example.org/test?a=b&amp;c=d">Link</a>
249249
250250
</body></html>"#
251251
);

0 commit comments

Comments
 (0)