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
3 changes: 3 additions & 0 deletions sapi/fuzzer/Makefile.frag
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ $(SAPI_FUZZER_PATH)/php-fuzz-mbstring: $(PHP_GLOBAL_OBJS) $(PHP_BINARY_OBJS) $(P

$(SAPI_FUZZER_PATH)/php-fuzz-mbregex: $(PHP_GLOBAL_OBJS) $(PHP_BINARY_OBJS) $(PHP_FUZZER_MBREGEX_OBJS)
$(FUZZER_BUILD) $(PHP_FUZZER_MBREGEX_OBJS) -o $@

$(SAPI_FUZZER_PATH)/php-fuzz-dom-html: $(PHP_GLOBAL_OBJS) $(PHP_BINARY_OBJS) $(PHP_FUZZER_DOM_HTML_OBJS)
$(FUZZER_BUILD) $(PHP_FUZZER_DOM_HTML_OBJS) -o $@
5 changes: 5 additions & 0 deletions sapi/fuzzer/config.m4
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,10 @@ if test "$PHP_FUZZER" != "no"; then
fi
fi

dnl HTML5 parsing of untrusted documents through the lexbor engine (ext/dom).
if test -n "$enable_dom" && test "$enable_dom" != "no"; then
PHP_FUZZER_TARGET([dom-html], [PHP_FUZZER_DOM_HTML_OBJS])
fi

PHP_SUBST([PHP_FUZZER_BINARIES])
fi
58 changes: 58 additions & 0 deletions sapi/fuzzer/dict/dom-html
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Tokens for HTML5 parsing (php-fuzz-dom-html).
"<html"
"</html"
"<head"
"</head"
"<body"
"</body"
"<title"
"<style"
"<script"
"<template"
"<svg"
"<math"
"<foreignObject"
"<table"
"<tbody"
"<tr"
"<td"
"<form"
"<input"
"<select"
"<option"
"<textarea"
"<iframe"
"<noscript"
"<frameset"
"<p"
"<br/>"
"<img"
" src="
" href="
" rel="
" type="
" charset="
" content="
" name="
" id="
" class="
" style="
" xmlns="
" xlink:href="
"<!--"
"-->"
"<!DOCTYPE html>"
"<![CDATA["
"<!ENTITY"
"<?php"
"<?xml"
"&amp;"
"&lt;"
"&#x41;"
"&quot;"
"&#0;"
"meta charset=\""
"http-equiv=\"Content-Type\""
"base href=\""
"srcset="
" autofocus onfocus=alert(1)//"
85 changes: 85 additions & 0 deletions sapi/fuzzer/fuzzer-dom-html.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
+----------------------------------------------------------------------+
| Copyright © The PHP Group and Contributors. |
| (drafted by the oss-fuzz-coverage-pipeline run 2026-11-09) |
+----------------------------------------------------------------------+
| This source file is subject to the Modified BSD License that is |
| bundled with this package in the file LICENSE, and is available |
| through the World Wide Web at <https://www.php.net/license/>. |
+----------------------------------------------------------------------+
| Authors: arthurscchan <github@arthurscchan> |
+----------------------------------------------------------------------+
*/

#include "fuzzer.h"

#include "main/php.h"
#include <main/php_config.h>
#include "main/php_main.h"

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

#include "fuzzer-sapi.h"

/* Large documents mostly slow down fuzzing without reaching new codepaths. */
#define MAX_SIZE (1 * 1024 * 1024)

/* createFromString()'s $options accept HTML_PARSE_* bits (LIBXML_NOERROR is
* 1<<5 = 32 in libxml2); 0 is the neutral default and HTML5 parsing is
* error-tolerant, so no suppression bit is needed. */
#define FUZZER_HTML_OPTIONS 0

int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
if (Size > MAX_SIZE || Size < 2) {
return 0;
}

if (fuzzer_request_startup() == FAILURE) {
return 0;
}

fuzzer_setup_dummy_frame();

zend_string *class_name = zend_string_init(ZEND_STRL("Dom\\HTMLDocument"), 0);
zend_class_entry *ce = zend_lookup_class(class_name);
zend_string_release(class_name);

if (ce) {
/* Method table entries are stored lowercased; lookups are case-sensitive. */
zend_function *create = zend_hash_str_find_ptr(&ce->function_table, ZEND_STRL("createfromstring"));
zend_function *save = zend_hash_str_find_ptr(&ce->function_table, ZEND_STRL("savehtml"));
ZEND_ASSERT(create != NULL);
ZEND_ASSERT(save != NULL);

zval retval, args[3];
ZVAL_STRINGL(&args[0], (const char *) Data, Size);
ZVAL_LONG(&args[1], FUZZER_HTML_OPTIONS);
ZVAL_NULL(&args[2]);

/* Parse untrusted bytes into an HTML5 document (lexbor engine). */
zend_call_known_function(create, NULL, ce, &retval, 3, args, NULL);

/* Round-trip direction: serialize the parsed document back to HTML. */
if (Z_TYPE(retval) == IS_OBJECT && save != NULL) {
zval retval2;
zend_call_known_instance_method_with_0_params(save, Z_OBJ_P(&retval), &retval2);
zval_ptr_dtor(&retval2);
}
zval_ptr_dtor(&retval);
zval_ptr_dtor(&args[0]);

if (EG(exception)) {
zend_clear_exception();
}
}

fuzzer_request_shutdown();
return 0;
}

int LLVMFuzzerInitialize(int *argc, char ***argv) {
fuzzer_init_php(NULL);
return 0;
}
5 changes: 5 additions & 0 deletions sapi/fuzzer/generate_all.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php
require __DIR__ . '/generate_unserialize_dict.php';
require __DIR__ . '/generate_mbstring_dict.php';
require __DIR__ . '/generate_domhtml_corpus.php';
require __DIR__ . '/generate_unserializehash_corpus.php';
require __DIR__ . '/generate_parser_corpus.php';

Expand All @@ -19,3 +20,7 @@
__DIR__ . '/corpus/tracing-jit',
["$baseDir/Zend/tests", "$baseDir/ext/opcache/tests/jit"]
);
generate_domhtml_corpus(
__DIR__ . '/corpus/dom-html',
["$baseDir/ext/dom/tests"]
);
39 changes: 39 additions & 0 deletions sapi/fuzzer/generate_domhtml_corpus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php
/* Assembles seeds for sapi/fuzzer/fuzzer-dom-html.c.
*
* The fuzzer feeds raw HTML bytes into Dom\HTMLDocument::createFromString() and
* serializes the result via saveHtml(). Seeds are the .html fixtures shipped with
* ext/dom tests, plus a few hand-built edge-case documents.
*/
function generate_domhtml_corpus(string $corpusDir, array $testDirs, int $maxLen = 512 * 1024) {
@mkdir($corpusDir, 0777, true);

$n = 0;
foreach ($testDirs as $testDir) {
if (!is_dir($testDir)) { continue; }
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($testDir));
foreach ($it as $file) {
if (!preg_match('/\.html$/i', (string) $file)) { continue; }
$content = file_get_contents((string) $file);
if ($content === false || $content === '' || strlen($content) > $maxLen) { continue; }
file_put_contents(sprintf('%s/domhtml_%03d.html', $corpusDir, $n++), $content);
}
}

/* Hand-built edge cases not present as fixtures. */
$extra = [
'<!DOCTYPE html><html><head><meta charset="windows-1252"><title>t</title></head>'
. '<body><p>caf&#233; &amp; &quot;quotes&quot; &#0;</p></body></html>',
'<body><svg><foreignObject><div xmlns="http://www.w3.org/1999/xhtml">x</div></foreignObject></svg>'
. '<math><mi>y</mi></math><template><tr><td>t</td></tr></template></body>',
'<table><tbody><tr><td><table><tr><td>deep</td></tr></table></td></tr></tbody></table>'
. '<p>unclosed <b>bold <i>both <span>spans',
'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"><html><!-- comment -- with -- dashes -->'
. '<head><script>if (1 < 2) { x = "</div>"; }</script><style>p:before { content: "</"; }</style></head></html>',
"\xEF\xBB\xBF<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">"
. '</head><body>' . str_repeat('<div class="x" data-y="1">d</div>', 8) . '</body></html>',
];
foreach ($extra as $content) {
file_put_contents(sprintf('%s/domhtml_extra_%02d.html', $corpusDir, $n++), $content);
}
}
Loading