LibWeb/HTML: Enforce constraints on range inputs

This extends the sanitization algorithm for range inputs in order to
prevent overflow, underflow and step mismatch.
This commit is contained in:
Glenn Skrzypczak 2025-08-10 22:35:28 +02:00 committed by Tim Flynn
commit aea8650d8b
Notes: github-actions[bot] 2025-08-14 15:07:02 +00:00
4 changed files with 99 additions and 34 deletions

View file

@ -1418,6 +1418,11 @@ void HTMLInputElement::form_associated_element_attribute_changed(FlyString const
m_value = value_sanitization_algorithm(m_value);
}
update_shadow_tree();
} else if (name == HTML::AttributeNames::min || name == HTML::AttributeNames::max || name == HTML::AttributeNames::step) {
if (type_state() == TypeAttributeState::Range) {
m_value = value_sanitization_algorithm(m_value);
update_shadow_tree();
}
}
}
@ -1716,19 +1721,59 @@ Utf16String HTMLInputElement::value_sanitization_algorithm(Utf16String const& va
}
// https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range):value-sanitization-algorithm
else if (type_state() == HTMLInputElement::TypeAttributeState::Range) {
// If the value of the element is not a valid floating-point number, then set it to the best representation, as
// a floating-point number, of the default value.
if (auto maybe_value = parse_floating_point_number(value); !is_valid_floating_point_number(value) ||
// AD-HOC: The spec doesn't require these checks - but other engines do them.
!maybe_value.has_value() || !isfinite(maybe_value.value())) {
// The default value is the minimum plus half the difference between the minimum and the maximum, unless the
// maximum is less than the minimum, in which case the default value is the minimum.
auto minimum = *min();
auto maximum = *max();
if (maximum < minimum)
return JS::number_to_utf16_string(minimum);
return JS::number_to_utf16_string(minimum + ((maximum - minimum) / 2.0));
auto minimum = *min();
auto maximum = *max();
auto number_value = [&value, &minimum, &maximum, this] {
// If the value of the element is not a valid floating-point number, then set it to the best representation,
// as a floating-point number, of the default value.
auto maybe_value = parse_floating_point_number(value);
if (!is_valid_floating_point_number(value) ||
// AD-HOC: The spec doesn't require these checks - but other engines do them.
!maybe_value.has_value() || !isfinite(maybe_value.value())) {
// The default value is the minimum plus half the difference between the minimum and the maximum, unless
// the maximum is less than the minimum, in which case the default value is the minimum.
if (maximum < minimum)
return minimum;
return minimum + ((maximum - minimum) / 2.0);
}
return maybe_value.value();
}();
// When the element is suffering from an overflow, if the maximum is not less than the minimum, the user agent
// must set the element's value to a valid floating-point number that represents the maximum.
if (is_number_overflowing(number_value)) {
number_value = max().value();
}
// When the element is suffering from an underflow, the user agent must set the element's value to the best
// representation, as a floating-point number, of the minimum.
if (is_number_underflowing(number_value)) {
number_value = min().value();
}
// When the element is suffering from a step mismatch, the user agent must round the element's value to the
// nearest number for which the element would not suffer from a step mismatch, and which is greater than or
// equal to the minimum, and, if the maximum is not less than the minimum, which is less than or equal to the
// maximum, if there is a number that matches these constraints. If two numbers match these constraints, then
// user agents must use the one nearest to positive infinity.
if (is_number_mismatching_step(number_value)) {
auto allowed_step = allowed_value_step().value();
auto minimum = min().value();
auto maximum = max().value();
auto step_mismatch = ((number_value - step_base()) / allowed_step) - floor((number_value - step_base()) / allowed_step);
if (step_mismatch >= 0.5) {
auto nearest_number = step_base() + (allowed_step * ceil((number_value - step_base()) / allowed_step));
if (nearest_number >= minimum && (maximum <= minimum || nearest_number <= maximum))
return JS::number_to_utf16_string(nearest_number);
}
if (step_mismatch <= 0.5) {
auto nearest_number = step_base() + (allowed_step * floor((number_value - step_base()) / allowed_step));
if (nearest_number >= minimum && (maximum <= minimum || nearest_number <= maximum))
return JS::number_to_utf16_string(nearest_number);
}
}
return JS::number_to_utf16_string(number_value);
}
// https://html.spec.whatwg.org/multipage/input.html#color-state-(type=color):value-sanitization-algorithm
else if (type_state() == HTMLInputElement::TypeAttributeState::Color) {
@ -2760,8 +2805,10 @@ WebIDL::ExceptionOr<void> HTMLInputElement::step_up_or_down(bool is_down, WebIDL
if (maybe_minimum.has_value() && maybe_maximum.has_value() && *maybe_minimum > *maybe_maximum)
return {};
// FIXME: 4. If the element has a minimum and a maximum and there is no value greater than or equal to the element's minimum and less than
// 4. If the element has a minimum and a maximum and there is no value greater than or equal to the element's minimum and less than
// or equal to the element's maximum that, when subtracted from the step base, is an integral multiple of the allowed value step, then return.
if (maybe_minimum.has_value() && maybe_maximum.has_value() && ceil((*maybe_minimum - step_base()) / allowed_value_step) > floor((*maybe_maximum - step_base()) / allowed_value_step))
return {};
// 5. If applying the algorithm to convert a string to a number to the string given by the element's value does not result in an error,
// then let value be the result of that algorithm. Otherwise, let value be zero.
@ -2796,14 +2843,14 @@ WebIDL::ExceptionOr<void> HTMLInputElement::step_up_or_down(bool is_down, WebIDL
// when subtracted from the step base, is an integral multiple of the allowed value step, and that is more than
// or equal to that minimum.
if (maybe_minimum.has_value() && value < *maybe_minimum) {
value = AK::max(value, *maybe_minimum);
value = step_base() + ceil((*maybe_minimum - step_base()) / allowed_value_step) * allowed_value_step;
}
// 9. If the element has a maximum, and value is greater than that maximum, then set value to the largest value that,
// when subtracted from the step base, is an integral multiple of the allowed value step, and that is less than
// or equal to that maximum.
if (maybe_maximum.has_value() && value > *maybe_maximum) {
value = AK::min(value, *maybe_maximum);
value = step_base() + floor((*maybe_maximum - step_base()) / allowed_value_step) * allowed_value_step;
}
// 10. If either the method invoked was the stepDown() method and value is greater than valueBeforeStepping,
@ -3408,6 +3455,11 @@ bool HTMLInputElement::suffering_from_an_underflow() const
if (!result.has_value())
return false;
auto number = result.value();
return is_number_underflowing(number);
}
bool HTMLInputElement::is_number_underflowing(double number) const
{
// https://html.spec.whatwg.org/multipage/input.html#the-min-and-max-attributes%3Asuffering-from-an-underflow-2
// When the element has a minimum and does not have a reversed range,
auto minimum = min();
@ -3436,6 +3488,11 @@ bool HTMLInputElement::suffering_from_an_overflow() const
if (!result.has_value())
return false;
auto number = result.value();
return is_number_overflowing(number);
}
bool HTMLInputElement::is_number_overflowing(double number) const
{
auto maximum = max();
// https://html.spec.whatwg.org/multipage/input.html#the-min-and-max-attributes%3Asuffering-from-an-overflow-2
// When the element has a maximum and does not have a reversed range,
@ -3458,17 +3515,22 @@ bool HTMLInputElement::suffering_from_an_overflow() const
// https://html.spec.whatwg.org/multipage/input.html#the-step-attribute%3Asuffering-from-a-step-mismatch
bool HTMLInputElement::suffering_from_a_step_mismatch() const
{
// When the element has an allowed value step,
auto maybe_allowed_value_step = allowed_value_step();
if (!maybe_allowed_value_step.has_value())
return false;
double allowed_value_step = *maybe_allowed_value_step;
// and the result of applying the algorithm to convert a string to a number to the string given by the element's
// value is a number,
auto maybe_number = convert_string_to_number(value());
if (!maybe_number.has_value())
return false;
double number = maybe_number.value();
return is_number_mismatching_step(number);
}
bool HTMLInputElement::is_number_mismatching_step(double number) const
{
// When the element has an allowed value step,
auto maybe_allowed_value_step = allowed_value_step();
if (!maybe_allowed_value_step.has_value())
return false;
double allowed_value_step = *maybe_allowed_value_step;
// and that number subtracted from the step base is not an integral multiple of the allowed value step, the element
// is suffering from a step mismatch.
return fmod(step_base() - number, allowed_value_step) != 0;