add el_html_sanitize allowlist runtime primitive
Replaces the need for product-level denylist sanitizers. Small state-machine parser; tag-and-attribute allowlist passed as JSON; URL scheme validation on href/src attrs (http, https, mailto, fragment, relative); whole-subtree drop for script/style/iframe/ object/embed/form (plus rarer media containers). No comment- wrapping (was fragile to comment-injection bypass via a literal --> inside an attacker-supplied attribute value). Also picks up the codegen and parser changes for first-class Instant/Duration types (postfix-literal time values, typed binop dispatch) that were sitting in tree alongside this work. Test corpus at tests/html_sanitizer/ covers the live attacker probes (script, iframe, form, javascript:, about:, data:, img onerror, onclick) plus structural attacks (comment-injection bypass, tab-in-scheme bypass, encoded payloads, malformed input, empty input, plain text). 29 cases, all green. Self-host fixed point holds at 5720 lines via the canonical el-compiler/src/compiler.el entry. Snapshot tagged at dist/platform/elc.20260502-1249-self-host. Backlog: bl-dc55ae07
This commit is contained in:
@@ -474,10 +474,55 @@ fn parse_block(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
|
||||
// ── Postfix expressions (calls, field access, index) ─────────────────────────
|
||||
|
||||
// is_duration_unit — recognise the postfix unit suffix on a numeric literal.
|
||||
// Used by parse_postfix to detect `30.seconds`-shape time literals before
|
||||
// falling back to the generic `obj.field` field-access lowering. Singular
|
||||
// and plural forms map to the same nanosecond multiplier; codegen does the
|
||||
// arithmetic at compile time.
|
||||
fn is_duration_unit(name: String) -> Bool {
|
||||
if name == "nanos" { return true }
|
||||
if name == "nano" { return true }
|
||||
if name == "millis" { return true }
|
||||
if name == "milli" { return true }
|
||||
if name == "millisecond" { return true }
|
||||
if name == "milliseconds" { return true }
|
||||
if name == "second" { return true }
|
||||
if name == "seconds" { return true }
|
||||
if name == "minute" { return true }
|
||||
if name == "minutes" { return true }
|
||||
if name == "hour" { return true }
|
||||
if name == "hours" { return true }
|
||||
if name == "day" { return true }
|
||||
if name == "days" { return true }
|
||||
false
|
||||
}
|
||||
|
||||
fn parse_postfix(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
let r = parse_primary(tokens, pos)
|
||||
let node = r["node"]
|
||||
let p = r["pos"]
|
||||
|
||||
// Postfix duration literal: `<Int>.<unit>` where <unit> is one of
|
||||
// nanos | millis | seconds | minutes | hours | days (each with an
|
||||
// optional plural). We recognise this before the generic Dot-as-field
|
||||
// path so `30.seconds` lowers to a DurationLit AST node carrying the
|
||||
// count and the unit, not a field access on an Int.
|
||||
let primary_kind: String = node["expr"]
|
||||
if primary_kind == "Int" {
|
||||
let dot_kind = tok_kind(tokens, p)
|
||||
if dot_kind == "Dot" {
|
||||
let unit_kind = tok_kind(tokens, p + 1)
|
||||
if unit_kind == "Ident" {
|
||||
let unit_name = tok_value(tokens, p + 1)
|
||||
if is_duration_unit(unit_name) {
|
||||
let count_str: String = node["value"]
|
||||
let node = { "expr": "DurationLit", "count": count_str, "unit": unit_name }
|
||||
let p = p + 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let running = true
|
||||
while running {
|
||||
let k = tok_kind(tokens, p)
|
||||
|
||||
Reference in New Issue
Block a user