Skip to content
Open
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
8 changes: 8 additions & 0 deletions inc/tag_replacer.php
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,8 @@ public function add_missing_srcset_attributes( $tag, $missing_srcsets, $new_url,
} else {
// Add new srcset attribute
$srcset_value = implode( ', ', $new_srcset_entries );
// Escape backreference metacharacters so this value can't be expanded by preg_replace() below.
$srcset_value = addcslashes( $srcset_value, '\\$' );
$srcset_attr = $is_slashed ? 'srcset=\"' . addcslashes( $srcset_value, '"' ) . '\"' : 'srcset="' . $srcset_value . '"';

// Insert srcset attribute after the src attribute
Expand Down Expand Up @@ -572,6 +574,8 @@ function ( $a, $b ) {
);

$sizes_value = implode( ', ', $sizes_entries );
// Escape backreference metacharacters before wrapping, see add_missing_srcset_attributes() above.
$sizes_value = addcslashes( $sizes_value, '\\$' );
$sizes_attr = $is_slashed ? 'sizes=\"' . addcslashes( $sizes_value, '"' ) . '\"' : 'sizes="' . $sizes_value . '"';

// Insert sizes attribute after srcset
Expand Down Expand Up @@ -626,6 +630,8 @@ function ( $a, $b ) {
);

$enhanced_srcset = implode( ', ', $all_entries );
// Escape backreference metacharacters so this value can't be expanded by preg_replace() below.
$enhanced_srcset = addcslashes( $enhanced_srcset, '\\$' );
$srcset_attr = $is_slashed ? 'srcset=\"' . addcslashes( $enhanced_srcset, '"' ) . '\"' : 'srcset="' . $enhanced_srcset . '"';

// Replace existing srcset
Expand Down Expand Up @@ -667,6 +673,8 @@ function ( $a, $b ) {
);

$enhanced_sizes = implode( ', ', $all_entries );
// Escape backreference metacharacters before wrapping, see enhance_existing_srcset() above.
$enhanced_sizes = addcslashes( $enhanced_sizes, '\\$' );
$sizes_attr = $is_slashed ? 'sizes=\"' . addcslashes( $enhanced_sizes, '"' ) . '\"' : 'sizes="' . $enhanced_sizes . '"';

// Replace existing sizes
Expand Down
155 changes: 154 additions & 1 deletion tests/test-srcset.php
Original file line number Diff line number Diff line change
Expand Up @@ -464,11 +464,164 @@ public function test_add_missing_srcset_attributes_adds_simple_sizes() {
}


/**
* Test that a "$0" srcset descriptor cannot inject a real onload attribute
*/
public function test_add_missing_srcset_attributes_prevents_backreference_injection() {
$tag_replacer = new Optml_Tag_Replacer();

$tag = '<img src="https://example.com/image.jpg" alt="Test" />';
$missing_srcsets = [
[ 'w' => 800, 'h' => 600, 'd' => 1, 's' => '$0 onload=alert(document.domain) x', 'b' => 0 ],
];

$result = $tag_replacer->add_missing_srcset_attributes( $tag, $missing_srcsets, 'https://example.com/image.jpg', false );
$img = $this->get_img_element( $result );

// The payload must stay inert literal text inside the srcset value, not become a real attribute
$this->assertFalse( $img->hasAttribute( 'onload' ) );
$this->assertStringContainsString( '$0 onload=alert(document.domain) x', $img->getAttribute( 'srcset' ) );
}

/**
* Test that a "$1" srcset descriptor cannot inject a real onerror attribute
*/
public function test_add_missing_srcset_attributes_prevents_dollar_one_injection() {
$tag_replacer = new Optml_Tag_Replacer();

$tag = '<img src="https://example.com/image.jpg" alt="Test" />';
$missing_srcsets = [
[ 'w' => 800, 'h' => 600, 'd' => 1, 's' => '$1 onerror=alert(1) x', 'b' => 0 ],
];

$result = $tag_replacer->add_missing_srcset_attributes( $tag, $missing_srcsets, 'https://example.com/image.jpg', false );
$img = $this->get_img_element( $result );

$this->assertFalse( $img->hasAttribute( 'onerror' ) );
$this->assertSame( 'https://example.com/image.jpg', $img->getAttribute( 'src' ) );
}

/**
* Test that enhance_existing_srcset does not expand a "$0" entry into a real attribute
*/
public function test_enhance_existing_srcset_prevents_backreference_injection() {
$tag_replacer = new Optml_Tag_Replacer();

$tag = '<img src="https://example.com/image.jpg" srcset="existing-300w.jpg 300w" alt="Test" />';
$new_entries = [ 'evil.jpg $0 onload=alert(document.domain) x' ];

$result = $tag_replacer->enhance_existing_srcset( $tag, $new_entries, false );
$img = $this->get_img_element( $result );

$this->assertFalse( $img->hasAttribute( 'onload' ) );
$this->assertStringContainsString( 'existing-300w.jpg 300w', $img->getAttribute( 'srcset' ) );
}

/**
* Test that enhance_existing_sizes does not expand a "$0" entry into a real attribute
*/
public function test_enhance_existing_sizes_prevents_backreference_injection() {
$tag_replacer = new Optml_Tag_Replacer();

$tag = '<img src="https://example.com/image.jpg" sizes="(max-width: 480px) 300px" alt="Test" />';
$new_entries = [ '$0 onload=alert(document.domain) x' ];

$result = $tag_replacer->enhance_existing_sizes( $tag, $new_entries, false );
$img = $this->get_img_element( $result );

$this->assertFalse( $img->hasAttribute( 'onload' ) );
$this->assertStringContainsString( '(max-width: 480px) 300px', $img->getAttribute( 'sizes' ) );
}

/**
* Test that a backslash-form "\0" srcset descriptor cannot inject a real onload attribute
*/
public function test_add_missing_srcset_attributes_prevents_backslash_backreference_injection() {
$tag_replacer = new Optml_Tag_Replacer();

$tag = '<img src="https://example.com/image.jpg" alt="Test" />';
$missing_srcsets = [
[ 'w' => 800, 'h' => 600, 'd' => 1, 's' => '\0 onload=alert(document.domain) x', 'b' => 0 ],
];

$result = $tag_replacer->add_missing_srcset_attributes( $tag, $missing_srcsets, 'https://example.com/image.jpg', false );
$img = $this->get_img_element( $result );

$this->assertFalse( $img->hasAttribute( 'onload' ) );
$this->assertStringContainsString( '\0 onload=alert(document.domain) x', $img->getAttribute( 'srcset' ) );
}

/**
* Test that enhance_existing_srcset does not expand a "\1" entry into a real attribute
*/
public function test_enhance_existing_srcset_prevents_backslash_backreference_injection() {
$tag_replacer = new Optml_Tag_Replacer();

$tag = '<img src="https://example.com/image.jpg" srcset="existing-300w.jpg 300w" alt="Test" />';
$new_entries = [ 'evil.jpg \1 onerror=alert(1) x' ];

$result = $tag_replacer->enhance_existing_srcset( $tag, $new_entries, false );
$img = $this->get_img_element( $result );

$this->assertFalse( $img->hasAttribute( 'onerror' ) );
$this->assertStringContainsString( 'existing-300w.jpg 300w', $img->getAttribute( 'srcset' ) );
}

/**
* Test that the slashed srcset output keeps its required escaping while a "$0" payload stays inert
*/
public function test_add_missing_srcset_attributes_slashed_prevents_backreference_injection() {
$tag_replacer = new Optml_Tag_Replacer();

$tag = '<img src="https://example.com/image.jpg" alt="Test" />';
$missing_srcsets = [
[ 'w' => 800, 'h' => 600, 'd' => 1, 's' => '$0 onload=alert(document.domain) x', 'b' => 0 ],
];

$result = $tag_replacer->add_missing_srcset_attributes( $tag, $missing_srcsets, 'https://example.com/image.jpg', true );

// The required slashed escaping around the srcset value must be preserved as-is (not doubled).
$this->assertStringContainsString( 'srcset=\"', $result );
$this->assertStringNotContainsString( 'srcset=\\\"', $result );
// The payload must stay inert literal text, not become a real attribute breakout.
$this->assertStringNotContainsString( '\" onload=', $result );
$this->assertStringContainsString( '$0 onload=alert(document.domain) x', $result );
}

/**
* Test that enhance_existing_srcset's slashed output keeps its required escaping while a "\0" payload stays inert
*/
public function test_enhance_existing_srcset_slashed_prevents_backslash_backreference_injection() {
$tag_replacer = new Optml_Tag_Replacer();

$tag = '<img src="https://example.com/image.jpg" srcset="existing-300w.jpg 300w" alt="Test" />';
$new_entries = [ 'evil.jpg \0 onload=alert(document.domain) x' ];

$result = $tag_replacer->enhance_existing_srcset( $tag, $new_entries, true );

$this->assertStringContainsString( 'srcset=\"', $result );
$this->assertStringNotContainsString( 'srcset=\\\"', $result );
$this->assertStringNotContainsString( '\" onload=', $result );
$this->assertStringContainsString( 'existing-300w.jpg 300w', $result );
}

/**
* Parse a single img tag string and return its DOM element
*/
private function get_img_element( $tag_html ) {
$dom = new DOMDocument();
$previous_setting = libxml_use_internal_errors( true );
$dom->loadHTML( $tag_html );
libxml_use_internal_errors( $previous_setting );

return $dom->getElementsByTagName( 'img' )->item( 0 );
}

/**
* Set up test environment
*/
public function setUp(): void {
parent::setUp();

}
}
Loading