selma 0.5.2 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Cargo.lock +1 -0
- data/README.md +4 -0
- data/ext/selma/Cargo.toml +1 -0
- data/ext/selma/src/lib.rs +1 -0
- data/ext/selma/src/rewriter.rs +119 -63
- data/ext/selma/src/sanitizer.rs +158 -119
- data/ext/selma/src/scan.rs +57 -0
- data/ext/selma/src/selector.rs +33 -26
- data/ext/selma/src/tags.rs +15 -1
- data/lib/selma/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 968ea670eb08eb49bbb5c305e257f87e4c38bd8371c9f0ba823600cc2be6c660
|
|
4
|
+
data.tar.gz: 83a5af8a2314d041ac6ae153db21271d57665c7b6a3ad851bdcb92c7753610d0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2673b5d305855725ffa53b343cdb6fa8da91903a914da50211eeff4acc17eee54837c29e6e8d19d20ad52c29075dc8ea2b2c9700b6cf0dfd255665aa0adfaccf
|
|
7
|
+
data.tar.gz: edd5982cef291e0fc666117c0ba0f6e7b18de03a007dc55b6a464b3fe0cb67db51435d86050ea01743694c65a8bd373debb606eff58a79a1e4fe361efa57912f
|
data/Cargo.lock
CHANGED
data/README.md
CHANGED
|
@@ -201,6 +201,10 @@ The structure of the `memory` options looks like this:
|
|
|
201
201
|
|
|
202
202
|
Note that `preallocated_parsing_buffer_size` must always be less than `max_allowed_memory_usage`. See [the`lol_html` project documentation](https://docs.rs/lol_html/1.2.1/lol_html/struct.MemorySettings.html) to learn more about the default values.
|
|
203
203
|
|
|
204
|
+
### Literal `<` in text is always escaped
|
|
205
|
+
|
|
206
|
+
Whenever a sanitizer is in use, a literal `<` in text is emitted as `<`. That includes `<textarea>` and `<title>`, where entities are still decoded, but not raw-text elements such as `<style>`, where they are not. This is what stops a stray `<` from fusing with the text after it into a brand new tag once the sanitizer removes whatever sat in between, e.g. `<<script></script>img src=x onerror=alert(1)>`. Your own `handle_text_chunk` handlers still receive the original text, and anything they `replace` takes precedence. An unterminated trailing token (`foo <!-- `, `foo <img src=x`) is dropped rather than passed through, since emitted verbatim it would keep parsing into whatever page the fragment is embedded in.
|
|
207
|
+
|
|
204
208
|
## Benchmarks
|
|
205
209
|
|
|
206
210
|
When `bundle exec rake benchmark`, two different benchmarks are calculated. Here are those results on my machine.
|
data/ext/selma/Cargo.toml
CHANGED
data/ext/selma/src/lib.rs
CHANGED
data/ext/selma/src/rewriter.rs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
use lol_html::{
|
|
2
|
-
doc_comments,
|
|
2
|
+
doc_comments, doc_text, doctype,
|
|
3
3
|
html_content::{Element, TextChunk},
|
|
4
|
-
|
|
5
|
-
Settings,
|
|
4
|
+
DocumentContentHandlers, ElementContentHandlers, HandlerResult, HtmlRewriter, MemorySettings,
|
|
5
|
+
Selector, Settings,
|
|
6
6
|
};
|
|
7
7
|
use magnus::{
|
|
8
8
|
function, gc, method,
|
|
@@ -21,16 +21,34 @@ use std::{
|
|
|
21
21
|
ops::Deref,
|
|
22
22
|
primitive::str,
|
|
23
23
|
rc::Rc,
|
|
24
|
+
sync::OnceLock,
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
use crate::{
|
|
27
28
|
html::{element::SelmaHTMLElement, end_tag::SelmaHTMLEndTag, text_chunk::SelmaHTMLTextChunk},
|
|
28
29
|
native_ref_wrap::NativeRefWrap,
|
|
29
30
|
sanitizer::SelmaSanitizer,
|
|
31
|
+
scan,
|
|
30
32
|
selector::SelmaSelector,
|
|
31
33
|
tags::Tag,
|
|
32
34
|
};
|
|
33
35
|
|
|
36
|
+
/// `*`, parsed once per process. lol_html's `element!` macro re-parses its CSS on every
|
|
37
|
+
/// call, which is measurable for rewriters that run on many small inputs.
|
|
38
|
+
fn all_elements_selector() -> &'static Selector {
|
|
39
|
+
static SELECTOR: OnceLock<Selector> = OnceLock::new();
|
|
40
|
+
SELECTOR.get_or_init(|| "*".parse().expect("`*` is a valid selector"))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fn escapeworthy_selector() -> &'static Selector {
|
|
44
|
+
static SELECTOR: OnceLock<Selector> = OnceLock::new();
|
|
45
|
+
SELECTOR.get_or_init(|| {
|
|
46
|
+
Tag::ESCAPEWORTHY_TAGS_CSS
|
|
47
|
+
.parse()
|
|
48
|
+
.expect("the escapeworthy tag list is valid CSS")
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
34
52
|
#[derive(Clone)]
|
|
35
53
|
pub struct Handler {
|
|
36
54
|
rb_handler: Opaque<Value>,
|
|
@@ -275,23 +293,37 @@ impl SelmaRewriter {
|
|
|
275
293
|
Ok(())
|
|
276
294
|
}));
|
|
277
295
|
}
|
|
278
|
-
if
|
|
296
|
+
if sanitizer.get_allow_comments() {
|
|
297
|
+
// a no-op handler still makes lol_html tokenize comments, so an
|
|
298
|
+
// unterminated `<!--`, `<!x` or `<?x` at EOF is dropped just like it is
|
|
299
|
+
// when comments are stripped, instead of being flushed raw by the
|
|
300
|
+
// tag-scanner fast path (which would comment out the host page)
|
|
301
|
+
sanitizer_document_content_handlers.push(doc_comments!(|_c| Ok(())));
|
|
302
|
+
} else {
|
|
279
303
|
sanitizer_document_content_handlers.push(doc_comments!(|c| {
|
|
280
304
|
sanitizer.remove_comment(c);
|
|
281
305
|
Ok(())
|
|
282
306
|
}));
|
|
283
307
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
}
|
|
308
|
+
// this must run in the same pass as element removal: by the time the
|
|
309
|
+
// output is re-parsed, a literal `<` next to a removed node has already
|
|
310
|
+
// fused with the text after it (see `SelmaSanitizer::escape_text_chunk`).
|
|
311
|
+
// Registering a text handler makes lol_html materialize every text chunk,
|
|
312
|
+
// so only pay for it when the input has a `<` that can end up in text.
|
|
313
|
+
if scan::has_escapable_lt(html.as_bytes()) {
|
|
314
|
+
sanitizer_document_content_handlers.push(doc_text!(|t| {
|
|
315
|
+
SelmaSanitizer::escape_text_chunk(t);
|
|
316
|
+
Ok(())
|
|
317
|
+
}));
|
|
318
|
+
}
|
|
319
|
+
sanitizer_element_content_handlers.push(Self::element_handler(
|
|
320
|
+
all_elements_selector(),
|
|
321
|
+
|el| {
|
|
322
|
+
sanitizer
|
|
323
|
+
.sanitize_element(el)
|
|
324
|
+
.map_err(|err| err.to_string().into())
|
|
325
|
+
},
|
|
326
|
+
));
|
|
295
327
|
}
|
|
296
328
|
};
|
|
297
329
|
|
|
@@ -305,13 +337,7 @@ impl SelmaRewriter {
|
|
|
305
337
|
html,
|
|
306
338
|
) {
|
|
307
339
|
Ok(rewritten_html) => match &binding.sanitizer {
|
|
308
|
-
None =>
|
|
309
|
-
Ok(output) => Ok(output),
|
|
310
|
-
Err(err) => Err(magnus::Error::new(
|
|
311
|
-
Ruby::get().unwrap().exception_runtime_error(),
|
|
312
|
-
format!("{err:?}"),
|
|
313
|
-
)),
|
|
314
|
-
},
|
|
340
|
+
None => Self::utf8(rewritten_html),
|
|
315
341
|
Some(sanitizer) => {
|
|
316
342
|
Self::perform_final_sanitization(self, sanitizer, rewritten_html)
|
|
317
343
|
}
|
|
@@ -327,30 +353,52 @@ impl SelmaRewriter {
|
|
|
327
353
|
sanitizer: &SelmaSanitizer,
|
|
328
354
|
html: Vec<u8>,
|
|
329
355
|
) -> Result<String, magnus::Error> {
|
|
330
|
-
//
|
|
331
|
-
|
|
356
|
+
// the only handler in this pass matches a fixed list of tag names, so a full
|
|
357
|
+
// re-parse is pointless unless the output can actually contain one of them
|
|
358
|
+
if !sanitizer.get_escape_tagfilter()
|
|
359
|
+
|| !scan::has_start_tag(&html, Tag::ESCAPEWORTHY_TAG_NAMES)
|
|
360
|
+
{
|
|
361
|
+
return Self::utf8(html);
|
|
362
|
+
}
|
|
332
363
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
let should_remove = sanitizer.allow_element(el);
|
|
336
|
-
if should_remove {
|
|
337
|
-
sanitizer.force_remove_element(el);
|
|
338
|
-
}
|
|
364
|
+
let element_content_handlers = vec![Self::element_handler(escapeworthy_selector(), |el| {
|
|
365
|
+
sanitizer.remove_if_disallowed(el);
|
|
339
366
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
367
|
+
Ok(())
|
|
368
|
+
})];
|
|
343
369
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
370
|
+
Self::run_rewrite(self, vec![], element_content_handlers, html.as_slice())
|
|
371
|
+
.and_then(Self::utf8)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/// Pairs a pre-parsed selector with an element handler; see `all_elements_selector`.
|
|
375
|
+
fn element_handler<'h>(
|
|
376
|
+
selector: &'h Selector,
|
|
377
|
+
handler: impl FnMut(&mut Element<'_, '_>) -> HandlerResult + 'h,
|
|
378
|
+
) -> (Cow<'h, Selector>, ElementContentHandlers<'h>) {
|
|
379
|
+
(
|
|
380
|
+
Cow::Borrowed(selector),
|
|
381
|
+
ElementContentHandlers::default().element(handler),
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
fn text_handler<'h>(
|
|
386
|
+
selector: &'h Selector,
|
|
387
|
+
handler: impl FnMut(&mut TextChunk<'_>) -> HandlerResult + 'h,
|
|
388
|
+
) -> (Cow<'h, Selector>, ElementContentHandlers<'h>) {
|
|
389
|
+
(
|
|
390
|
+
Cow::Borrowed(selector),
|
|
391
|
+
ElementContentHandlers::default().text(handler),
|
|
392
|
+
)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
fn utf8(html: Vec<u8>) -> Result<String, magnus::Error> {
|
|
396
|
+
String::from_utf8(html).map_err(|err| {
|
|
397
|
+
magnus::Error::new(
|
|
398
|
+
Ruby::get().unwrap().exception_runtime_error(),
|
|
399
|
+
format!("{err:?}"),
|
|
400
|
+
)
|
|
401
|
+
})
|
|
354
402
|
}
|
|
355
403
|
|
|
356
404
|
pub fn perform_handler_rewrite<'a>(
|
|
@@ -372,10 +420,10 @@ impl SelmaRewriter {
|
|
|
372
420
|
let selector = &handler.selector;
|
|
373
421
|
|
|
374
422
|
// TODO: test final raise by simulating errors
|
|
375
|
-
if let Some(match_element) = selector.
|
|
423
|
+
if let Some(match_element) = selector.element_selector() {
|
|
376
424
|
let closure_element_stack = element_stack.clone();
|
|
377
425
|
|
|
378
|
-
element_content_handlers.push(
|
|
426
|
+
element_content_handlers.push(Self::element_handler(match_element, move |el| {
|
|
379
427
|
match Self::process_element_handlers(
|
|
380
428
|
handler,
|
|
381
429
|
el,
|
|
@@ -387,10 +435,10 @@ impl SelmaRewriter {
|
|
|
387
435
|
}));
|
|
388
436
|
}
|
|
389
437
|
|
|
390
|
-
if let Some(match_text_within) = selector.
|
|
438
|
+
if let Some(match_text_within) = selector.text_selector() {
|
|
391
439
|
let closure_element_stack = element_stack.clone();
|
|
392
440
|
|
|
393
|
-
element_content_handlers.push(
|
|
441
|
+
element_content_handlers.push(Self::text_handler(match_text_within, move |text| {
|
|
394
442
|
let element_stack = closure_element_stack.as_ref().borrow();
|
|
395
443
|
// check if current tag is a tag we should be ignoring text within;
|
|
396
444
|
// also checks if tag is within an ancestery of ignored tags
|
|
@@ -408,27 +456,31 @@ impl SelmaRewriter {
|
|
|
408
456
|
}
|
|
409
457
|
|
|
410
458
|
// we need to check *every* element we iterate over, to create a stack of elements
|
|
411
|
-
element_content_handlers.push(
|
|
412
|
-
|
|
459
|
+
element_content_handlers.push(Self::element_handler(
|
|
460
|
+
all_elements_selector(),
|
|
461
|
+
move |el| {
|
|
462
|
+
// lol_html already lowercases the name
|
|
463
|
+
let tag_name = el.tag_name();
|
|
464
|
+
|
|
465
|
+
// no need to track self-closing tags
|
|
466
|
+
if Tag::tag_from_tag_name(&tag_name).self_closing {
|
|
467
|
+
return Ok(());
|
|
468
|
+
};
|
|
413
469
|
|
|
414
|
-
|
|
415
|
-
if Tag::tag_from_tag_name(&tag_name).self_closing {
|
|
416
|
-
return Ok(());
|
|
417
|
-
};
|
|
470
|
+
element_stack.as_ref().borrow_mut().push(tag_name);
|
|
418
471
|
|
|
419
|
-
|
|
472
|
+
let closure_element_stack = element_stack.clone();
|
|
420
473
|
|
|
421
|
-
|
|
474
|
+
let handler: lol_html::EndTagHandler<'static> = Box::new(move |_end_tag| {
|
|
475
|
+
closure_element_stack.as_ref().borrow_mut().pop();
|
|
476
|
+
Ok(())
|
|
477
|
+
});
|
|
478
|
+
// ignore void elements (lol_html's void list may differ from selma's `self_closing`)
|
|
479
|
+
let _ = el.on_end_tag(handler);
|
|
422
480
|
|
|
423
|
-
let handler: lol_html::EndTagHandler<'static> = Box::new(move |_end_tag| {
|
|
424
|
-
closure_element_stack.as_ref().borrow_mut().pop();
|
|
425
481
|
Ok(())
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
let _ = el.on_end_tag(handler);
|
|
429
|
-
|
|
430
|
-
Ok(())
|
|
431
|
-
}));
|
|
482
|
+
},
|
|
483
|
+
));
|
|
432
484
|
});
|
|
433
485
|
|
|
434
486
|
Self::run_rewrite(
|
|
@@ -467,6 +519,10 @@ impl SelmaRewriter {
|
|
|
467
519
|
));
|
|
468
520
|
}
|
|
469
521
|
}
|
|
522
|
+
// deliberately no `rewriter.end()`: lol_html would flush whatever token it is
|
|
523
|
+
// still holding at EOF *verbatim*, which turns a half-open `<img src=x onerror=..`
|
|
524
|
+
// into live markup once the fragment is embedded in a page; dropping an
|
|
525
|
+
// unterminated trailing token is the safe choice for a sanitizer
|
|
470
526
|
}
|
|
471
527
|
Ok(output)
|
|
472
528
|
}
|
data/ext/selma/src/sanitizer.rs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
use std::{borrow::
|
|
1
|
+
use std::{borrow::Cow, collections::HashMap, sync::OnceLock};
|
|
2
2
|
|
|
3
3
|
use lol_html::{
|
|
4
4
|
errors::AttributeNameError,
|
|
5
|
-
html_content::{Comment, ContentType, Doctype, Element, EndTag},
|
|
5
|
+
html_content::{Comment, ContentType, Doctype, Element, EndTag, TextChunk},
|
|
6
6
|
};
|
|
7
7
|
use magnus::{
|
|
8
8
|
eval, function, method,
|
|
@@ -15,11 +15,19 @@ use magnus::{
|
|
|
15
15
|
#[derive(Clone, Debug, Default)]
|
|
16
16
|
struct ElementSanitizer {
|
|
17
17
|
allowed_attrs: Vec<String>,
|
|
18
|
-
required_attrs: Vec<String>,
|
|
19
18
|
allowed_classes: Vec<String>,
|
|
20
19
|
protocol_sanitizers: HashMap<String, Vec<String>>,
|
|
21
20
|
}
|
|
22
21
|
|
|
22
|
+
impl ElementSanitizer {
|
|
23
|
+
/// Shared stand-in for elements the config never mentions, so lookups at rewrite
|
|
24
|
+
/// time never insert anything and the config stays immutable once built.
|
|
25
|
+
fn empty() -> &'static ElementSanitizer {
|
|
26
|
+
static EMPTY: OnceLock<ElementSanitizer> = OnceLock::new();
|
|
27
|
+
EMPTY.get_or_init(ElementSanitizer::default)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
#[derive(Clone)]
|
|
24
32
|
pub struct Sanitizer {
|
|
25
33
|
flags: [u8; crate::tags::Tag::TAG_COUNT],
|
|
@@ -33,9 +41,11 @@ pub struct Sanitizer {
|
|
|
33
41
|
config: Opaque<RHash>,
|
|
34
42
|
}
|
|
35
43
|
|
|
44
|
+
/// The config is fully built in `new` and never changes afterwards, so there is no
|
|
45
|
+
/// interior mutability here: every rewrite-time method only reads.
|
|
36
46
|
#[derive(Clone)]
|
|
37
47
|
#[magnus::wrap(class = "Selma::Sanitizer")]
|
|
38
|
-
pub struct SelmaSanitizer(
|
|
48
|
+
pub struct SelmaSanitizer(Sanitizer);
|
|
39
49
|
|
|
40
50
|
impl SelmaSanitizer {
|
|
41
51
|
const SELMA_SANITIZER_ALLOW: u8 = (1 << 0);
|
|
@@ -65,16 +75,10 @@ impl SelmaSanitizer {
|
|
|
65
75
|
}
|
|
66
76
|
};
|
|
67
77
|
|
|
78
|
+
// only elements the config actually mentions get an entry; everything else
|
|
79
|
+
// resolves to `ElementSanitizer::empty()` at rewrite time
|
|
68
80
|
let mut element_sanitizers = HashMap::new();
|
|
69
81
|
|
|
70
|
-
// TODO: set up default tags; do we need this?
|
|
71
|
-
crate::tags::Tag::html_tags().iter().for_each(|html_tag| {
|
|
72
|
-
let element_name = crate::tags::Tag::element_name_from_enum(html_tag).to_string();
|
|
73
|
-
|
|
74
|
-
let element_sanitizer = ElementSanitizer::default();
|
|
75
|
-
element_sanitizers.insert(element_name, element_sanitizer);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
82
|
// def allow_attribute(element, attrs)
|
|
79
83
|
// attrs.flatten.each { |attr| set_allowed_attribute(element, attr, true) }
|
|
80
84
|
// end
|
|
@@ -172,7 +176,7 @@ impl SelmaSanitizer {
|
|
|
172
176
|
None => true,
|
|
173
177
|
};
|
|
174
178
|
|
|
175
|
-
Ok(Self(
|
|
179
|
+
Ok(Self(Sanitizer {
|
|
176
180
|
flags,
|
|
177
181
|
allowed_attrs: sanitizer_allowed_attrs,
|
|
178
182
|
allowed_classes: sanitizer_allowed_classes,
|
|
@@ -182,7 +186,7 @@ impl SelmaSanitizer {
|
|
|
182
186
|
allow_comments,
|
|
183
187
|
allow_doctype,
|
|
184
188
|
config: config.into(),
|
|
185
|
-
}))
|
|
189
|
+
}))
|
|
186
190
|
}
|
|
187
191
|
|
|
188
192
|
fn setup_config(
|
|
@@ -276,10 +280,9 @@ impl SelmaSanitizer {
|
|
|
276
280
|
}
|
|
277
281
|
|
|
278
282
|
fn get_config(&self) -> Result<RHash, magnus::Error> {
|
|
279
|
-
let binding = self.0.borrow();
|
|
280
283
|
let ruby = Ruby::get().unwrap();
|
|
281
284
|
|
|
282
|
-
Ok(ruby.get_inner(
|
|
285
|
+
Ok(ruby.get_inner(self.0.config))
|
|
283
286
|
}
|
|
284
287
|
|
|
285
288
|
/// Toggle a sanitizer option on or off.
|
|
@@ -317,7 +320,7 @@ impl SelmaSanitizer {
|
|
|
317
320
|
}
|
|
318
321
|
|
|
319
322
|
pub fn escape_tagfilter(&self, e: &mut Element) -> bool {
|
|
320
|
-
if self.0.
|
|
323
|
+
if self.0.escape_tagfilter {
|
|
321
324
|
let tag = crate::tags::Tag::tag_from_element(e);
|
|
322
325
|
if crate::tags::Tag::is_tag_escapeworthy(tag) {
|
|
323
326
|
e.remove();
|
|
@@ -329,11 +332,30 @@ impl SelmaSanitizer {
|
|
|
329
332
|
}
|
|
330
333
|
|
|
331
334
|
pub fn get_escape_tagfilter(&self) -> bool {
|
|
332
|
-
self.0.
|
|
335
|
+
self.0.escape_tagfilter
|
|
333
336
|
}
|
|
334
337
|
|
|
335
338
|
pub fn get_allow_comments(&self) -> bool {
|
|
336
|
-
self.0.
|
|
339
|
+
self.0.allow_comments
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/// A `<` that the tokenizer classified as text (because the character after it
|
|
343
|
+
/// cannot start a tag, e.g. `<<b>` or `< b`) is otherwise passed through verbatim.
|
|
344
|
+
/// If the sanitizer then removes the node that follows it, the text on either side
|
|
345
|
+
/// joins up and re-tokenizes as markup.
|
|
346
|
+
/// Escaping `<` in every context that decodes entities keeps
|
|
347
|
+
/// text as text regardless of what gets removed around it. Raw-text contexts
|
|
348
|
+
/// (`<script>`, `<style>`, ...) are skipped: entities are not decoded there, so
|
|
349
|
+
/// escaping would corrupt the content rather than protect it.
|
|
350
|
+
pub fn escape_text_chunk(text_chunk: &mut TextChunk) {
|
|
351
|
+
if !text_chunk.text_type().allows_html_entities() {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if text_chunk.as_str().contains('<') {
|
|
356
|
+
let escaped = text_chunk.as_str().replace('<', "<");
|
|
357
|
+
text_chunk.set_str(escaped);
|
|
358
|
+
}
|
|
337
359
|
}
|
|
338
360
|
|
|
339
361
|
pub fn remove_comment(&self, c: &mut Comment) {
|
|
@@ -342,7 +364,7 @@ impl SelmaSanitizer {
|
|
|
342
364
|
|
|
343
365
|
/// Whether or not to keep HTML doctype.
|
|
344
366
|
pub fn get_allow_doctype(&self) -> bool {
|
|
345
|
-
self.0.
|
|
367
|
+
self.0.allow_doctype
|
|
346
368
|
}
|
|
347
369
|
|
|
348
370
|
pub fn remove_doctype(&self, d: &mut Doctype) {
|
|
@@ -355,7 +377,7 @@ impl SelmaSanitizer {
|
|
|
355
377
|
allow_list: RArray,
|
|
356
378
|
) {
|
|
357
379
|
let ruby = Ruby::get().unwrap();
|
|
358
|
-
let protocol_sanitizers = &mut element_sanitizer.protocol_sanitizers
|
|
380
|
+
let protocol_sanitizers = &mut element_sanitizer.protocol_sanitizers;
|
|
359
381
|
|
|
360
382
|
for allowed_protocol in allow_list.into_iter() {
|
|
361
383
|
let protocol_list = protocol_sanitizers.get_mut(&attr_name);
|
|
@@ -397,92 +419,95 @@ impl SelmaSanitizer {
|
|
|
397
419
|
}
|
|
398
420
|
}
|
|
399
421
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
422
|
+
/// Everything the sanitizer does to one element, with a single tag lookup: remove it
|
|
423
|
+
/// (and, depending on the config, its contents) when it is not allowed, otherwise
|
|
424
|
+
/// filter and re-escape its attributes.
|
|
425
|
+
pub fn sanitize_element(&self, element: &mut Element) -> Result<(), AttributeNameError> {
|
|
426
|
+
// `tag_name()` allocates, so take it once and derive everything else from it
|
|
427
|
+
let name = element.tag_name();
|
|
428
|
+
let tag = crate::tags::Tag::tag_from_tag_name(&name);
|
|
429
|
+
|
|
430
|
+
self.try_remove_element(element, tag);
|
|
431
|
+
if element.removed() {
|
|
432
|
+
// nothing left to sanitize
|
|
433
|
+
return Ok(());
|
|
434
|
+
}
|
|
408
435
|
|
|
409
|
-
|
|
436
|
+
self.sanitize_attributes(element, tag, &name)
|
|
437
|
+
}
|
|
410
438
|
|
|
411
|
-
|
|
412
|
-
|
|
439
|
+
fn sanitize_attributes(
|
|
440
|
+
&self,
|
|
441
|
+
element: &mut Element,
|
|
442
|
+
tag: crate::tags::Tag,
|
|
443
|
+
tag_name: &str,
|
|
444
|
+
) -> Result<(), AttributeNameError> {
|
|
445
|
+
let sanitizer = &self.0;
|
|
446
|
+
let element_sanitizer = sanitizer
|
|
447
|
+
.element_sanitizers
|
|
448
|
+
.get(tag_name)
|
|
449
|
+
.unwrap_or_else(|| ElementSanitizer::empty());
|
|
450
|
+
|
|
451
|
+
// the attribute list cannot be iterated while the element is being mutated, so
|
|
452
|
+
// take a snapshot. Every occurrence of a duplicated name is evaluated in order;
|
|
453
|
+
// a rejected occurrence removes the attribute outright.
|
|
454
|
+
let attributes: Vec<(String, String)> = element
|
|
413
455
|
.attributes()
|
|
414
456
|
.iter()
|
|
415
457
|
.map(|a| (a.name(), a.value()))
|
|
416
458
|
.collect();
|
|
417
459
|
|
|
418
|
-
for (attr_name, attr_val) in
|
|
460
|
+
for (attr_name, attr_val) in &attributes {
|
|
419
461
|
// you can actually embed <!-- ... --> inside
|
|
420
462
|
// an HTML tag to pass malicious data. If this is
|
|
421
463
|
// encountered, remove the entire element to be safe.
|
|
422
464
|
if attr_name.starts_with("<!--") {
|
|
423
|
-
Self::force_remove_element(
|
|
465
|
+
Self::force_remove_element(element, tag);
|
|
424
466
|
return Ok(());
|
|
425
467
|
}
|
|
426
468
|
|
|
427
|
-
// first, trim leading spaces and unescape any encodings
|
|
469
|
+
// first, trim leading spaces and unescape any encodings (an entity always
|
|
470
|
+
// starts with `&`, so a value without one is already unescaped)
|
|
428
471
|
let trimmed = attr_val.trim_start();
|
|
429
|
-
let
|
|
430
|
-
|
|
472
|
+
let unescaped_attr_val: Cow<str> = if trimmed.contains('&') {
|
|
473
|
+
let bytes = escapist::unescape_html(trimmed.as_bytes());
|
|
474
|
+
Cow::Owned(
|
|
475
|
+
String::from_utf8(bytes)
|
|
476
|
+
.unwrap_or_else(|err| String::from_utf8_lossy(err.as_bytes()).into_owned()),
|
|
477
|
+
)
|
|
478
|
+
} else {
|
|
479
|
+
Cow::Borrowed(trimmed)
|
|
480
|
+
};
|
|
431
481
|
|
|
432
|
-
let
|
|
433
|
-
|
|
482
|
+
let keep = Self::should_keep_attribute(
|
|
483
|
+
sanitizer,
|
|
434
484
|
element,
|
|
435
|
-
|
|
485
|
+
element_sanitizer,
|
|
436
486
|
attr_name,
|
|
437
487
|
&unescaped_attr_val,
|
|
438
|
-
)
|
|
439
|
-
Ok(should_keep) => should_keep,
|
|
440
|
-
Err(e) => {
|
|
441
|
-
return Err(e);
|
|
442
|
-
}
|
|
443
|
-
};
|
|
488
|
+
)?;
|
|
444
489
|
|
|
445
|
-
if !
|
|
490
|
+
if !keep {
|
|
446
491
|
element.remove_attribute(attr_name);
|
|
447
|
-
|
|
448
|
-
// Prevent the use of `<meta>` elements that set a charset other than UTF-8,
|
|
449
|
-
// since output is always UTF-8.
|
|
450
|
-
if crate::tags::Tag::is_meta(tag) {
|
|
451
|
-
if attr_name == "charset" && unescaped_attr_val != "utf-8" {
|
|
452
|
-
match element.set_attribute(attr_name, "utf-8") {
|
|
453
|
-
Ok(_) => {}
|
|
454
|
-
Err(err) => {
|
|
455
|
-
return Err(err);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
} else if !unescaped_attr_val.is_empty() {
|
|
460
|
-
let mut buf = String::new();
|
|
461
|
-
// ...then, escape any special characters, for security
|
|
462
|
-
if attr_name == "href" {
|
|
463
|
-
escapist::escape_href(&mut buf, unescaped_attr_val.as_str()).unwrap();
|
|
464
|
-
} else {
|
|
465
|
-
escapist::escape_html(&mut buf, unescaped_attr_val.as_str()).unwrap();
|
|
466
|
-
};
|
|
467
|
-
|
|
468
|
-
match element.set_attribute(attr_name, &buf) {
|
|
469
|
-
Ok(_) => {}
|
|
470
|
-
Err(err) => {
|
|
471
|
-
return Err(err);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
492
|
+
continue;
|
|
475
493
|
}
|
|
476
|
-
}
|
|
477
494
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
if
|
|
485
|
-
|
|
495
|
+
// Prevent the use of `<meta>` elements that set a charset other than UTF-8,
|
|
496
|
+
// since output is always UTF-8.
|
|
497
|
+
if crate::tags::Tag::is_meta(tag) {
|
|
498
|
+
if attr_name == "charset" && unescaped_attr_val != "utf-8" {
|
|
499
|
+
element.set_attribute(attr_name, "utf-8")?;
|
|
500
|
+
}
|
|
501
|
+
} else if !unescaped_attr_val.is_empty() {
|
|
502
|
+
// ...then, escape any special characters, for security
|
|
503
|
+
let mut buf = String::with_capacity(unescaped_attr_val.len());
|
|
504
|
+
if attr_name == "href" {
|
|
505
|
+
escapist::escape_href(&mut buf, &unescaped_attr_val).unwrap();
|
|
506
|
+
} else {
|
|
507
|
+
escapist::escape_html(&mut buf, &unescaped_attr_val).unwrap();
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
element.set_attribute(attr_name, &buf)?;
|
|
486
511
|
}
|
|
487
512
|
}
|
|
488
513
|
|
|
@@ -493,12 +518,15 @@ impl SelmaSanitizer {
|
|
|
493
518
|
binding: &Sanitizer,
|
|
494
519
|
element: &mut Element,
|
|
495
520
|
element_sanitizer: &ElementSanitizer,
|
|
496
|
-
attr_name: &
|
|
521
|
+
attr_name: &str,
|
|
497
522
|
attr_val: &str,
|
|
498
523
|
) -> Result<bool, AttributeNameError> {
|
|
499
524
|
let mut allowed: bool = false;
|
|
500
|
-
let element_allowed_attrs = element_sanitizer
|
|
501
|
-
|
|
525
|
+
let element_allowed_attrs = element_sanitizer
|
|
526
|
+
.allowed_attrs
|
|
527
|
+
.iter()
|
|
528
|
+
.any(|a| a == attr_name);
|
|
529
|
+
let sanitizer_allowed_attrs = binding.allowed_attrs.iter().any(|a| a == attr_name);
|
|
502
530
|
|
|
503
531
|
if element_allowed_attrs {
|
|
504
532
|
allowed = true;
|
|
@@ -547,7 +575,7 @@ impl SelmaSanitizer {
|
|
|
547
575
|
}
|
|
548
576
|
|
|
549
577
|
fn has_allowed_protocol(protocols_allowed: &[String], attr_val: &str) -> bool {
|
|
550
|
-
if protocols_allowed.
|
|
578
|
+
if protocols_allowed.iter().any(|p| p == "all") {
|
|
551
579
|
return true;
|
|
552
580
|
}
|
|
553
581
|
|
|
@@ -558,10 +586,18 @@ impl SelmaSanitizer {
|
|
|
558
586
|
};
|
|
559
587
|
|
|
560
588
|
match attr_val.as_bytes()[idx] {
|
|
561
|
-
b'/' => protocols_allowed.
|
|
562
|
-
b'#' => protocols_allowed.
|
|
563
|
-
// Allow protocol name to be case-insensitive
|
|
564
|
-
_ =>
|
|
589
|
+
b'/' => protocols_allowed.iter().any(|p| p == "/"),
|
|
590
|
+
b'#' => protocols_allowed.iter().any(|p| p == "#"),
|
|
591
|
+
// Allow protocol name to be case-insensitive (the config side is taken as-is)
|
|
592
|
+
_ => {
|
|
593
|
+
let protocol = &attr_val.as_bytes()[..idx];
|
|
594
|
+
protocols_allowed.iter().any(|p| {
|
|
595
|
+
p.len() == protocol.len()
|
|
596
|
+
&& p.bytes()
|
|
597
|
+
.zip(protocol)
|
|
598
|
+
.all(|(allowed, given)| allowed == given.to_ascii_lowercase())
|
|
599
|
+
})
|
|
600
|
+
}
|
|
565
601
|
}
|
|
566
602
|
}
|
|
567
603
|
|
|
@@ -574,8 +610,6 @@ impl SelmaSanitizer {
|
|
|
574
610
|
) -> Result<bool, lol_html::errors::AttributeNameError> {
|
|
575
611
|
let allowed_global = &binding.allowed_classes;
|
|
576
612
|
|
|
577
|
-
let mut valid_classes: Vec<String> = vec![];
|
|
578
|
-
|
|
579
613
|
let allowed_local = &element_sanitizer.allowed_classes;
|
|
580
614
|
|
|
581
615
|
// No class filters, so everything goes through
|
|
@@ -583,15 +617,13 @@ impl SelmaSanitizer {
|
|
|
583
617
|
return Ok(true);
|
|
584
618
|
}
|
|
585
619
|
|
|
586
|
-
let
|
|
587
|
-
attr_value
|
|
620
|
+
let valid_classes: Vec<&str> = attr_val
|
|
588
621
|
.split_whitespace()
|
|
589
|
-
.
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
});
|
|
622
|
+
.filter(|class| {
|
|
623
|
+
allowed_global.iter().any(|a| a == class)
|
|
624
|
+
|| allowed_local.iter().any(|a| a == class)
|
|
625
|
+
})
|
|
626
|
+
.collect();
|
|
595
627
|
|
|
596
628
|
if valid_classes.is_empty() {
|
|
597
629
|
return Ok(false);
|
|
@@ -603,18 +635,23 @@ impl SelmaSanitizer {
|
|
|
603
635
|
}
|
|
604
636
|
}
|
|
605
637
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
let flags: u8 = self.0.borrow().flags[tag.index];
|
|
609
|
-
|
|
610
|
-
(flags & Self::SELMA_SANITIZER_ALLOW) == 0
|
|
638
|
+
fn is_disallowed(&self, tag: crate::tags::Tag) -> bool {
|
|
639
|
+
(self.0.flags[tag.index] & Self::SELMA_SANITIZER_ALLOW) == 0
|
|
611
640
|
}
|
|
612
641
|
|
|
613
|
-
|
|
642
|
+
/// The final pass only needs to know whether a (possibly handler-inserted or fused)
|
|
643
|
+
/// element from the tagfilter list is allowed, and remove it outright if not.
|
|
644
|
+
pub fn remove_if_disallowed(&self, element: &mut Element) {
|
|
614
645
|
let tag = crate::tags::Tag::tag_from_element(element);
|
|
615
|
-
|
|
646
|
+
if self.is_disallowed(tag) {
|
|
647
|
+
Self::force_remove_element(element, tag);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
616
650
|
|
|
617
|
-
|
|
651
|
+
fn try_remove_element(&self, element: &mut Element, tag: crate::tags::Tag) -> bool {
|
|
652
|
+
let flags: u8 = self.0.flags[tag.index];
|
|
653
|
+
|
|
654
|
+
let should_remove = !element.removed() && self.is_disallowed(tag);
|
|
618
655
|
|
|
619
656
|
if should_remove {
|
|
620
657
|
if crate::tags::Tag::has_text_content(tag) {
|
|
@@ -627,11 +664,11 @@ impl SelmaSanitizer {
|
|
|
627
664
|
Self::remove_element(element, tag.self_closing, flags);
|
|
628
665
|
}
|
|
629
666
|
|
|
630
|
-
Self::check_if_end_tag_needs_removal(element);
|
|
667
|
+
Self::check_if_end_tag_needs_removal(element, tag);
|
|
631
668
|
} else {
|
|
632
669
|
// anything in <iframe> must be removed, if it's kept
|
|
633
670
|
if crate::tags::Tag::is_iframe(tag) {
|
|
634
|
-
if
|
|
671
|
+
if flags != 0 {
|
|
635
672
|
element.set_inner_content(" ", ContentType::Text);
|
|
636
673
|
} else {
|
|
637
674
|
element.set_inner_content("", ContentType::Text);
|
|
@@ -662,15 +699,17 @@ impl SelmaSanitizer {
|
|
|
662
699
|
}
|
|
663
700
|
}
|
|
664
701
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
702
|
+
fn force_remove_element(element: &mut Element, tag: crate::tags::Tag) {
|
|
703
|
+
Self::remove_element(
|
|
704
|
+
element,
|
|
705
|
+
tag.self_closing,
|
|
706
|
+
Self::SELMA_SANITIZER_REMOVE_CONTENTS,
|
|
707
|
+
);
|
|
708
|
+
Self::check_if_end_tag_needs_removal(element, tag);
|
|
670
709
|
}
|
|
671
710
|
|
|
672
|
-
fn check_if_end_tag_needs_removal(element: &mut Element) {
|
|
673
|
-
if element.removed() && !
|
|
711
|
+
fn check_if_end_tag_needs_removal(element: &mut Element, tag: crate::tags::Tag) {
|
|
712
|
+
if element.removed() && !tag.self_closing {
|
|
674
713
|
// ignore void elements (lol_html's void list may differ from selma's `self_closing`)
|
|
675
714
|
let _ = element.on_end_tag(Box::new(move |end| {
|
|
676
715
|
Self::remove_end_tag(end);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
//! Cheap byte scans that decide whether a rewriting pass has any work to do, so the
|
|
2
|
+
//! expensive part (a full lol_html parse, or materializing every text chunk) is only
|
|
3
|
+
//! paid for when it can matter.
|
|
4
|
+
|
|
5
|
+
use memchr::memchr;
|
|
6
|
+
|
|
7
|
+
fn starts_with_ignore_ascii_case(haystack: &[u8], needle: &[u8]) -> bool {
|
|
8
|
+
haystack.len() >= needle.len() && haystack[..needle.len()].eq_ignore_ascii_case(needle)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/// Whether any `<` in `html` can end up in an entity-decoding text chunk, which is the
|
|
12
|
+
/// only place `SelmaSanitizer::escape_text_chunk` has work to do.
|
|
13
|
+
///
|
|
14
|
+
/// In the data state the tokenizer emits a `<` as text exactly when the byte after it
|
|
15
|
+
/// can start neither a tag (ASCII letter), a comment or doctype (`!`), an end tag (`/`),
|
|
16
|
+
/// nor a bogus comment (`?`). Inside `<textarea>` and `<title>` (RCDATA) every `<` is
|
|
17
|
+
/// text, so the presence of either start tag counts as well. Raw-text contexts such as
|
|
18
|
+
/// `<script>` and `<style>` are never escaped and need no consideration here.
|
|
19
|
+
///
|
|
20
|
+
/// A false positive only costs registering the handler; there are no false negatives.
|
|
21
|
+
pub fn has_escapable_lt(html: &[u8]) -> bool {
|
|
22
|
+
let mut pos = 0;
|
|
23
|
+
while let Some(i) = memchr(b'<', &html[pos..]) {
|
|
24
|
+
let at = pos + i;
|
|
25
|
+
match html.get(at + 1) {
|
|
26
|
+
Some(b'!' | b'/' | b'?') => {}
|
|
27
|
+
Some(c) if c.is_ascii_alphabetic() => {
|
|
28
|
+
let name = &html[at + 1..];
|
|
29
|
+
if starts_with_ignore_ascii_case(name, b"textarea")
|
|
30
|
+
|| starts_with_ignore_ascii_case(name, b"title")
|
|
31
|
+
{
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// anything else (or nothing at all) after a `<` makes it text
|
|
36
|
+
_ => return true,
|
|
37
|
+
}
|
|
38
|
+
pos = at + 1;
|
|
39
|
+
}
|
|
40
|
+
false
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Whether `html` contains a start tag for any of `names`, compared ASCII
|
|
44
|
+
/// case-insensitively. Matching by prefix over-approximates (`<titlex>` counts for
|
|
45
|
+
/// `title`), which is the safe direction for a gate.
|
|
46
|
+
pub fn has_start_tag(html: &[u8], names: &[&[u8]]) -> bool {
|
|
47
|
+
let mut pos = 0;
|
|
48
|
+
while let Some(i) = memchr(b'<', &html[pos..]) {
|
|
49
|
+
let at = pos + i;
|
|
50
|
+
let name = &html[at + 1..];
|
|
51
|
+
if names.iter().any(|n| starts_with_ignore_ascii_case(name, n)) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
pos = at + 1;
|
|
55
|
+
}
|
|
56
|
+
false
|
|
57
|
+
}
|
data/ext/selma/src/selector.rs
CHANGED
|
@@ -3,8 +3,9 @@ use magnus::{function, scan_args, Error, Module, Object, RModule, Ruby, Value};
|
|
|
3
3
|
#[derive(Clone, Debug)]
|
|
4
4
|
#[magnus::wrap(class = "Selma::Selector")]
|
|
5
5
|
pub struct SelmaSelector {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
// parsed once here, so `Rewriter#rewrite` does not re-parse the CSS on every call
|
|
7
|
+
element_selector: Option<lol_html::Selector>,
|
|
8
|
+
text_selector: Option<lol_html::Selector>,
|
|
8
9
|
ignore_text_within: Option<Vec<String>>,
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -23,25 +24,31 @@ impl SelmaSelector {
|
|
|
23
24
|
));
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
let element_selector = match &match_element {
|
|
28
|
+
None => None,
|
|
29
|
+
Some(css) => match css.parse::<lol_html::Selector>() {
|
|
30
|
+
Ok(selector) => Some(selector),
|
|
31
|
+
Err(_) => {
|
|
32
|
+
return Err(Error::new(
|
|
33
|
+
ruby.exception_arg_error(),
|
|
34
|
+
format!("Could not parse `match_element` (`{css:?}`) as valid CSS"),
|
|
35
|
+
));
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
};
|
|
36
39
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
let text_selector = match &match_text_within {
|
|
41
|
+
None => None,
|
|
42
|
+
Some(css) => match css.parse::<lol_html::Selector>() {
|
|
43
|
+
Ok(selector) => Some(selector),
|
|
44
|
+
Err(_) => {
|
|
45
|
+
return Err(Error::new(
|
|
46
|
+
ruby.exception_arg_error(),
|
|
47
|
+
format!("Could not parse `match_text_within` (`{css:?}`) as valid CSS"),
|
|
48
|
+
));
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
45
52
|
|
|
46
53
|
let ignore_text_within = match rb_ignore_text_within {
|
|
47
54
|
None => None,
|
|
@@ -57,8 +64,8 @@ impl SelmaSelector {
|
|
|
57
64
|
};
|
|
58
65
|
|
|
59
66
|
Ok(Self {
|
|
60
|
-
|
|
61
|
-
|
|
67
|
+
element_selector,
|
|
68
|
+
text_selector,
|
|
62
69
|
ignore_text_within,
|
|
63
70
|
})
|
|
64
71
|
}
|
|
@@ -87,12 +94,12 @@ impl SelmaSelector {
|
|
|
87
94
|
Ok((match_element, match_text_within, rb_ignore_text_within))
|
|
88
95
|
}
|
|
89
96
|
|
|
90
|
-
pub fn
|
|
91
|
-
self.
|
|
97
|
+
pub fn element_selector(&self) -> Option<&lol_html::Selector> {
|
|
98
|
+
self.element_selector.as_ref()
|
|
92
99
|
}
|
|
93
100
|
|
|
94
|
-
pub fn
|
|
95
|
-
self.
|
|
101
|
+
pub fn text_selector(&self) -> Option<&lol_html::Selector> {
|
|
102
|
+
self.text_selector.as_ref()
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
pub fn ignore_text_within(&self) -> Option<&[String]> {
|
data/ext/selma/src/tags.rs
CHANGED
|
@@ -208,12 +208,26 @@ impl Tag {
|
|
|
208
208
|
pub const ESCAPEWORTHY_TAGS_CSS: &'static str =
|
|
209
209
|
"title, textarea, style, xmp, iframe, noembed, noframes, script, plaintext";
|
|
210
210
|
|
|
211
|
+
/// Byte-level twin of [`Self::ESCAPEWORTHY_TAGS_CSS`], used to decide whether the
|
|
212
|
+
/// final sanitization pass has anything to do before paying for a full re-parse.
|
|
213
|
+
pub const ESCAPEWORTHY_TAG_NAMES: &'static [&'static [u8]] = &[
|
|
214
|
+
b"title",
|
|
215
|
+
b"textarea",
|
|
216
|
+
b"style",
|
|
217
|
+
b"xmp",
|
|
218
|
+
b"iframe",
|
|
219
|
+
b"noembed",
|
|
220
|
+
b"noframes",
|
|
221
|
+
b"script",
|
|
222
|
+
b"plaintext",
|
|
223
|
+
];
|
|
224
|
+
|
|
211
225
|
pub fn html_tags() -> Vec<HTMLTag> {
|
|
212
226
|
all::<HTMLTag>().collect::<Vec<_>>()
|
|
213
227
|
}
|
|
214
228
|
|
|
215
229
|
pub fn tag_from_element(element: &mut Element) -> Tag {
|
|
216
|
-
Self::tag_from_tag_name(element.tag_name()
|
|
230
|
+
Self::tag_from_tag_name(&element.tag_name())
|
|
217
231
|
}
|
|
218
232
|
|
|
219
233
|
pub fn tag_from_tag_name(tag_name: &str) -> Tag {
|
data/lib/selma/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: selma
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Garen J. Torikian
|
|
@@ -44,6 +44,7 @@ files:
|
|
|
44
44
|
- ext/selma/src/native_ref_wrap.rs
|
|
45
45
|
- ext/selma/src/rewriter.rs
|
|
46
46
|
- ext/selma/src/sanitizer.rs
|
|
47
|
+
- ext/selma/src/scan.rs
|
|
47
48
|
- ext/selma/src/selector.rs
|
|
48
49
|
- ext/selma/src/tags.rs
|
|
49
50
|
- lib/selma.rb
|
|
@@ -84,7 +85,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
84
85
|
- !ruby/object:Gem::Version
|
|
85
86
|
version: '3.4'
|
|
86
87
|
requirements: []
|
|
87
|
-
rubygems_version: 4.0.
|
|
88
|
+
rubygems_version: 4.0.20
|
|
88
89
|
specification_version: 4
|
|
89
90
|
summary: Selma selects and matches HTML nodes using CSS rules. Backed by Rust's lol_html
|
|
90
91
|
parser.
|