Always neutralize javascript: links, independent of fixRelativeURLs#42
Merged
fivefilters merged 1 commit intoJul 14, 2026
Conversation
Readability.js always strips javascript: anchors in _postProcessContent via _fixRelativeUris. This port gated the entire fixRelativeUris() step behind the fixRelativeURLs config flag (default false), so with the default configuration javascript: links passed straight through to the output — a regression from upstream. The test corpus masked this because its harness always enables fixRelativeURLs (jsdom always has a base URI). Decouple the two concerns: javascript: neutralization now always runs (it needs no base URL and is a defense-in-depth measure), while absolutizing relative URLs stays opt-in via fixRelativeURLs. Add a regression test exercising the default configuration, and expand the README/UPGRADE security notes to spell out what does and does not survive extraction (event handlers, data: URIs on media, whitelisted video embeds) so callers still run a real sanitizer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WPggv1W7LrNiWLDrHas5WU
489e696
into
claude/readability-php-modernize-ii46mk
2 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A security review of the library turned up one concrete, user-facing regression from upstream Readability.js. The input/parsing side is otherwise in good shape (no XXE — the native
\Dom\HTMLDocument/Lexbor parser has no DTD/entity surface; no SSRF — nothing is fetched; no ReDoS in the built-in regexes; no dynamic-exec sinks).The regression: Readability.js always runs
_fixRelativeUrisinside_postProcessContent, which neutralizes<a href="javascript:...">links. This port gated the entirefixRelativeUris()step behind thefixRelativeURLsconfig flag, which defaults tofalse. So with the default configuration,javascript:links passed straight through toArticle::$content, unlike upstream.The test corpus masked this because its harness always sets
fixRelativeURLs: true(test/ReadabilityTest.php:130), mirroring the fact that jsdom always supplies a base URI.What changed
src/Readability.php— decouple the two concerns inpostProcessContent()/fixRelativeUris(). Thejavascript:neutralization now always runs (it needs no base URL and is a defense-in-depth measure); absolutizing relativehref/src/srcset/postervalues stays opt-in behindfixRelativeURLs && baseURI !== null.test/ReadabilityUnitTest.php— add a regression test that parses ajavascript:anchor with the defaultConfigurationand asserts the scheme is gone while the link text survives. Verified it fails before the fix and passes after.README.md/UPGRADE.md— expand the Security notes: state plainly that the output is extracted, not sanitized, and enumerate what survives extraction (inlineon*handlers,data:/non-http URIs on media and lazy images, whitelisted video<iframe>/<embed>/<object>), plus operational guidance onmaxElemsToParseand treatingallowedVideoRegexas trusted config.This intentionally does not add on-handler stripping or cross-sink scheme filtering — that would deviate from Readability's deliberate "extractor, not sanitizer" design, which the README already directs callers to pair with a real sanitizer.
Testing
./vendor/bin/phpunit— all 473 tests pass, including the full Mozilla corpus (thejs-link-replacementfixture, which runs withfixRelativeURLson, is unaffected)../vendor/bin/psalm— no errors.🤖 Generated with Claude Code
https://claude.ai/code/session_01WPggv1W7LrNiWLDrHas5WU
Generated by Claude Code