Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 6 additions & 10 deletions components/DataLiberation/CSS/class-cssprocessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -1564,13 +1564,11 @@ private function decode_string_or_url( int $start, int $length ): string {
$end = $start + $length;

while ( $at < $end ) {
// Find next special character.
$normal_len = strcspn( $this->css, $special_chars, $at );
// Find next special character within the token boundary.
$normal_len = strcspn( $this->css, $special_chars, $at, $end - $at );
if ( $normal_len > 0 ) {
// Clamp to not exceed the end boundary.
$normal_len = min( $normal_len, $end - $at );
$decoded .= substr( $this->css, $at, $normal_len );
$at += $normal_len;
$decoded .= substr( $this->css, $at, $normal_len );
$at += $normal_len;
}

if ( $at >= $end ) {
Expand Down Expand Up @@ -1648,11 +1646,9 @@ private function decode_escape_at( int $offset, &$bytes_consumed ): string {
return "\u{FFFD}";
}

// Hex digits.
$hex_len = strspn( $this->css, '0123456789ABCDEFabcdef', $at );
// Hex digits (CSS spec allows at most 6).
$hex_len = strspn( $this->css, '0123456789ABCDEFabcdef', $at, 6 );
if ( $hex_len > 0 ) {
// Consume up to 6 hex digits.
$hex_len = min( $hex_len, 6 );
$hex = substr( $this->css, $at, $hex_len );
$at += $hex_len;

Expand Down
42 changes: 42 additions & 0 deletions components/DataLiberation/Tests/CSSProcessorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1556,4 +1556,46 @@ public function test_bad_string_token_value_is_null(): void {
$this->assertSame( CSSProcessor::TOKEN_BAD_STRING, $processor->get_token_type() );
$this->assertNull( $processor->get_token_value() );
}

/**
* Tests that decode_string_or_url() respects the token's length boundary
* and does not include content from beyond the token end.
*
* The escape sequence \41 (= 'A') triggers the slow path in
* decode_string_or_url(). The CSS string continues with "; color: red;"
* after the closing quote, which must not appear in the token value.
*/
public function test_decode_string_or_url_respects_length_boundary(): void {
// \41 = 'A' — triggers the slow path; "; color: red;" follows the token.
$css = '"hello\\41 world"; color: red;';

$processor = CSSProcessor::create( $css );
$processor->next_token();

$this->assertSame( CSSProcessor::TOKEN_STRING, $processor->get_token_type() );
$this->assertSame( 'helloAworld', $processor->get_token_value() );
$this->assertSame( '"helloAworld"', $processor->get_normalized_token() );
}

/**
* Tests that decode_escape_at() consumes at most 6 hex digits, as required
* by the CSS Syntax Level 3 specification.
*
* A hex escape with 7 consecutive hex digits must only consume the first 6,
* leaving the 7th as a literal character in the string value.
*
* @see https://www.w3.org/TR/css-syntax-3/#consume-escaped-code-point
*/
public function test_decode_escape_at_hex_limit_is_six_digits(): void {
// \000041 is 6 hex digits → U+0041 = 'A'; the trailing '1' is literal.
// Without the length limit, strspn() would scan 7 hex digits (0000411),
// giving U+0411 = 'Б' (Cyrillic), which is incorrect.
$css = '"\\0000411rest"';

$processor = CSSProcessor::create( $css );
$processor->next_token();

$this->assertSame( CSSProcessor::TOKEN_STRING, $processor->get_token_type() );
$this->assertSame( 'A1rest', $processor->get_token_value() );
}
}