LibWeb: Only allow ASFs in descriptor values if explicitly supported

`@function` descriptors are the only ones that support ASFs, while most
descriptors enforce this through their syntaxes implicitly disallowing
ASFs, this wasn't the case for `@property/initial-value`.

We now explictly disallow ASFs unless they are marked as allowed within
`Descriptors.json`.
This commit is contained in:
Callum Law
2026-03-28 21:02:41 +13:00
committed by Sam Atkins
parent 8b66e7f463
commit 071b000d9f
Notes: github-actions[bot] 2026-03-30 18:58:58 +00:00
8 changed files with 84 additions and 15 deletions

View File

@@ -183,21 +183,23 @@ Each at-rule object has the following fields. Both are required.
Each descriptor object can have the following fields:
| Field | Required | Description |
|--------------------|----------|-----------------------------------------------------------------------|
| `initial` | No | String. The descriptor's initial value if none is provided. |
| `legacy-alias-for` | No | String. The name of a different descriptor that this is an alias for. |
| `syntax` | Yes | Array of strings. Each string is one option, taken from the spec. |
| `FIXME` or `NOTE` | No | Strings, for when you want to leave a note. |
| Field | Default | Required | Description |
|------------------------------------------|---------|----------|----------------------------------------------------------------------------|
| `allow-arbitrary-substitution-functions` | `false` | No | Boolean. Whether this descriptor supports arbitrary substitution functions |
| `initial` | Nothing | No | String. The descriptor's initial value if none is provided. |
| `legacy-alias-for` | Nothing | No | String. The name of a different descriptor that this is an alias for. |
| `syntax` | N/A | Yes | Array of strings. Each string is one option, taken from the spec. |
| `FIXME` or `NOTE` | Nothing | No | Strings, for when you want to leave a note. |
### Custom descriptor fields
Each custom descriptor object has the following fields
| Field | Required | Description |
|--------------------|----------|----------------------------------------------|
| `syntax` | Yes | Array of strings. Each string is one option. |
| `FIXME` or `NOTE` | No | Strings, for when you want to leave a note. |
| Field | Default | Required | Description |
|------------------------------------------|---------|----------|----------------------------------------------------------------------------|
| `allow-arbitrary-substitution-functions` | `false` | No | Boolean. Whether this descriptor supports arbitrary substitution functions |
| `syntax` | N/A | Yes | Array of strings. Each string is one option. |
| `FIXME` or `NOTE` | Nothing | No | Strings, for when you want to leave a note. |
## Keywords.json

View File

@@ -261,12 +261,14 @@
"spec": "https://drafts.csswg.org/css-mixins-1/#function-rule",
"descriptors": {
"result": {
"allow-arbitrary-substitution-functions": true,
"syntax": [
"<declaration-value>?"
]
}
},
"custom-descriptors": {
"allow-arbitrary-substitution-functions": true,
"syntax": [
"<declaration-value>?"
]

View File

@@ -36,20 +36,46 @@ Parser::ParseErrorOr<NonnullRefPtr<StyleValue const>> Parser::parse_descriptor_v
auto transaction = tokens.begin_transaction();
auto descriptor_value_start_index = tokens.current_index();
SubstitutionFunctionsPresence substitution_functions_presence {};
tokens.mark();
while (tokens.has_next_token()) {
auto const& token = tokens.consume_a_token();
if (token.is(Token::Type::Semicolon))
return ParseError::SyntaxError;
collect_arbitrary_substitution_function_presence(token, substitution_functions_presence);
}
auto metadata = get_descriptor_metadata(at_rule_id, descriptor_name_and_id.id());
if (substitution_functions_presence.has_any()) {
// https://drafts.csswg.org/css-values-5/#resolve-property
// Unless otherwise specified, arbitrary substitution functions can be used in place of any part of any
// propertys value (including within other functional notations); and are not valid in any other context.
// NB: Since we are not in a property value context we only allow ASFs if they are explicitly allowed in
// Descriptors.json
if (!metadata.allow_arbitrary_substitution_functions) {
ErrorReporter::the().report(InvalidValueError {
.value_type = MUST(String::formatted("{}/{}", to_string(at_rule_id), descriptor_name_and_id.name())),
.value_string = tokens.dump_string(),
.description = "ASFs are not supported in this descriptor"_string,
});
return ParseError::SyntaxError;
}
return UnresolvedStyleValue::create(Vector<ComponentValue> { tokens.tokens_since(descriptor_value_start_index) }, substitution_functions_presence);
}
tokens.restore_a_mark();
Optional<ComputationContext> computation_context = m_document
? ComputationContext { .length_resolution_context = Length::ResolutionContext::for_document(*m_document) }
: Optional<ComputationContext> {};
auto metadata = get_descriptor_metadata(at_rule_id, descriptor_name_and_id.id());
for (auto const& option : metadata.syntax) {
auto syntax_transaction = transaction.create_child();
auto parsed_style_value = option.visit(
@@ -61,10 +87,8 @@ Parser::ParseErrorOr<NonnullRefPtr<StyleValue const>> Parser::parse_descriptor_v
if (value_or_error.is_error())
return nullptr;
auto value_for_property = value_or_error.release_value();
// Descriptors don't accept the following, which properties do:
// - CSS-wide keywords
// - Arbitrary substitution functions (so, UnresolvedStyleValue)
if (value_for_property->is_css_wide_keyword() || value_for_property->is_unresolved())
// Descriptors don't accept the CSS-wide keywords
if (value_for_property->is_css_wide_keyword())
return nullptr;
return value_for_property;
},

View File

@@ -138,6 +138,7 @@ struct DescriptorMetadata {
UnicodeRangeTokens,
};
Vector<Variant<Keyword, PropertyID, ValueType>> syntax;
bool allow_arbitrary_substitution_functions { false };
};
DescriptorMetadata get_descriptor_metadata(AtRuleID, DescriptorID);
@@ -468,7 +469,11 @@ DescriptorMetadata get_descriptor_metadata(AtRuleID at_rule_id, DescriptorID des
generate_syntax_list(descriptor_generator, syntax);
descriptor_generator.set("allow-arbitrary-substitution-functions"sv, descriptor.get_bool("allow-arbitrary-substitution-functions"sv).value_or(false) ? "true" : "false");
descriptor_generator.append(R"~~~(
metadata.allow_arbitrary_substitution_functions = @allow-arbitrary-substitution-functions@;
return metadata;
}
)~~~");
@@ -484,7 +489,12 @@ DescriptorMetadata get_descriptor_metadata(AtRuleID at_rule_id, DescriptorID des
)~~~");
auto const& syntax = custom_descriptors.get_array("syntax"sv).value();
generate_syntax_list(custom_descriptor_generator, syntax);
custom_descriptor_generator.set("allow-arbitrary-substitution-functions"sv, custom_descriptors.get_bool("allow-arbitrary-substitution-functions"sv).value_or(false) ? "true" : "false");
custom_descriptor_generator.append(R"~~~(
metadata.allow_arbitrary_substitution_functions = @allow-arbitrary-substitution-functions@;
return metadata;
}
)~~~");

View File

@@ -0,0 +1 @@
@function --foo(--bar) { --baz: var(--bar); result: var(--baz); }

View File

@@ -0,0 +1 @@
@property --foo { syntax: "*"; inherits: true; }

View File

@@ -0,0 +1,14 @@
<!doctype html>
<style>
@function --foo(--bar) {
--baz: var(--bar);
result: var(--baz);
}
</style>
<div id="target"></div>
<script src="../include.js"></script>
<script>
test(() => {
println(document.styleSheets[0].cssRules[0].cssText);
});
</script>

View File

@@ -0,0 +1,15 @@
<!doctype html>
<style>
@property --foo {
syntax: "*";
inherits: true;
initial-value: var(foo);
}
</style>
<div id="target"></div>
<script src="../include.js"></script>
<script>
test(() => {
println(document.styleSheets[0].cssRules[0].cssText);
});
</script>