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::$maxInputLength defaults to 20_000 as a DOS guard for web-form input. HtmlSanitizerConfigBuilder::build() never overrides it, so every sanitizer instance silently inherits the cap. Input is substr()-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)) since html_processor is a server-side content pipeline — source URLs are administrator-configured, not anonymous-user-supplied, and content is already scoped by the container step before reaching the sanitizer.
  • Exposes maxInputLength as a first-class sanitizer option so callers in untrusted contexts can set an explicit byte limit. Use -1 to disable entirely (the new module default) or set a positive integer (e.g. 65536).

Changes

  • HtmlSanitizerConfigBuilder — adds KNOWN_ALIASES = ['maxInputLength'] constant; initializes config with ->withMaxInputLength(-1); adds maxInputLength branch in processOption() with integer validation
  • HtmlSanitizerConfigBuilderTest — adds four tests: default is unlimited (24 kB input survives), explicit cap truncates output, explicit -1 is unlimited, invalid value (-2) throws HtmlProcessorException
  • HtmlProcessor — updates getPipelineOptionsSchema() sanitizer description to document the maxInputLength sub-key
  • README.md — adds sanitizer input length note to Security considerations

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 maxInputLength and >= -1

PHPUnit (run from ../html_processor)

ddev phpunit tests/src/Unit/HtmlSanitizerConfigBuilderTest.php
  • All tests pass, including the four new maxInputLength tests
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

Merge request reports

Loading