Issue #3607368: Fix HtmlSanitizerConfigBuilder silently truncating input at Symfony's 20,000-byte default
Summary
- Fixes silent content truncation when HTML passed to the sanitizer exceeds 20,000 bytes. Symfony's
HtmlSanitizerConfig::$maxInputLengthdefaults to20_000as a DOS guard for web-form input.HtmlSanitizerConfigBuilder::build()never overrides it, so every sanitizer instance silently inherits the cap. Input issubstr()-cut at the byte boundary before DOM parsing; DOMDocument then parses the broken markup, auto-closes all open tags, and serializes content-incomplete output. No warning is logged, no exception is thrown, and the migration reports success. - Disables the cap by default (
withMaxInputLength(-1)) sincehtml_processoris a server-side content pipeline — source URLs are administrator-configured, not anonymous-user-supplied, and content is already scoped by thecontainerstep before reaching the sanitizer. - Exposes
maxInputLengthas a first-class sanitizer option so callers in untrusted contexts can set an explicit byte limit. Use-1to disable entirely (the new module default) or set a positive integer (e.g.65536).
Changes
HtmlSanitizerConfigBuilder— addsKNOWN_ALIASES = ['maxInputLength']constant; initializes config with->withMaxInputLength(-1); addsmaxInputLengthbranch inprocessOption()with integer validationHtmlSanitizerConfigBuilderTest— adds four tests: default is unlimited (24 kB input survives), explicit cap truncates output, explicit-1is unlimited, invalid value (-2) throwsHtmlProcessorExceptionHtmlProcessor— updatesgetPipelineOptionsSchema()sanitizer description to document themaxInputLengthsub-keyREADME.md— adds sanitizer input length note to Security considerations
Related
Companion issue: HtmlSanitizerConfigBuilder silently inherits Symfony's 20,000-byte input cap, causing content truncation with no warning filed against html_processor. Discovered during regression analysis of complex_content_migration — the live .editionAbout section on Open Library is ~40 kB; with the default cap active only ~half the edition rows were returned with no error.
Test plan
Commands use drush eval to exercise the sanitizer option directly. PHPUnit covers the unit-level assertions.
Default — large input is not truncated
ddev drush eval "
\$content = '<div>' . str_repeat('<p>' . str_repeat('x', 10) . '</p>', 2500) . '</div>';
\$processor = \Drupal::service(\Drupal\html_processor\HtmlProcessorInterface::class);
\$result = \$processor->process([
'content' => \$content,
'container' => ['div'],
'sanitizer' => ['allowSafeElements' => TRUE],
]);
echo 'in=' . strlen(\$content) . ' out=' . strlen(\$result) . PHP_EOL;
"-
in=value is greater than 20,000 -
out=value is greater than 20,000 (no truncation)
Explicit cap — output is bounded
ddev drush eval "
\$content = '<div>' . str_repeat('<p>' . str_repeat('x', 10) . '</p>', 2500) . '</div>';
\$processor = \Drupal::service(\Drupal\html_processor\HtmlProcessorInterface::class);
\$result = \$processor->process([
'content' => \$content,
'container' => ['div'],
'sanitizer' => ['allowSafeElements' => TRUE, 'maxInputLength' => 500],
]);
echo 'in=' . strlen(\$content) . ' out=' . strlen(\$result) . PHP_EOL;
"-
out=value is well below 1,000 (cap applied before DOM parse)
Explicit -1 — behaves the same as the default
ddev drush eval "
\$content = '<div>' . str_repeat('<p>' . str_repeat('x', 10) . '</p>', 2500) . '</div>';
\$processor = \Drupal::service(\Drupal\html_processor\HtmlProcessorInterface::class);
\$result = \$processor->process([
'content' => \$content,
'container' => ['div'],
'sanitizer' => ['allowSafeElements' => TRUE, 'maxInputLength' => -1],
]);
echo 'in=' . strlen(\$content) . ' out=' . strlen(\$result) . PHP_EOL;
"-
out=value is greater than 20,000
Invalid value — exception surfaced
ddev drush eval "
\$processor = \Drupal::service(\Drupal\html_processor\HtmlProcessorInterface::class);
\$processor->process([
'content' => '<p>test</p>',
'sanitizer' => ['allowSafeElements' => TRUE, 'maxInputLength' => -2],
]);
"- Error output contains
maxInputLengthand>= -1
PHPUnit (run from ../html_processor)
ddev phpunit tests/src/Unit/HtmlSanitizerConfigBuilderTest.php- All tests pass, including the four new
maxInputLengthtests
ddev phpunit tests/src/Kernel/HtmlProcessorRealWorldRegressionTest.php- Both fixture cases pass (Martian Chronicles 10-row output, Time Machine full output)
Closes #3607368
Edited by William Estrada