You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[security] GHSA-h78x-47qg-qjmq: Escape output and sanitize href in DataObject Data\Link::getHtml - #19484
Pimcore\Model\DataObject\Data\Link::getHtml() (and __toString()) concatenated href, the named attributes (title, class, rel, target, tabindex, accesskey) and the free-form attributes string directly into the <a> tag without escaping — only the link text went through htmlspecialchars(). The URI scheme of href was never validated either. Since this data type is populated from editor/end-user input, a stored value could:
break out of an attribute value (" in href/title/class) to inject arbitrary markup or event handlers,
use a javascript: URI so the link executes script on click,
inject event-handler attributes (e.g. onclick=...) or break out of the tag via the free-form attributes string.
This is the DataObject sibling of GHSA-97cp-8873-v2gf (Document Link XSS) and uses the same mitigation approach as the Document Link editable fix in #19392 (GHSA-9g27-c28m-8xg5).
Fix
The mitigation is opt-in on this release line. It reuses the Pimcore\Model\Document\Editable\Link\AttributeSanitizer policy class from #19392, but is enabled by its own, independent config (so DataObject links and Document links can be switched separately):
pimcore:
objects:
link_sanitizer:
strict: true# optional, only used while strict is true:blocked_url_schemes: ['javascript:', 'vbscript:'] # default; entries are validatedblock_unsafe_data_urls: true # default
With strict: true, getHtml():
renders an empty href for javascript:/vbscript: (and the configured schemes) and for script-capable data: URLs (data:text/html, data:image/svg+xml, ...); whitespace/control-character and character-reference obfuscation (java\tscript:, javascript:) is covered by the shared policy,
parses the free-form attributes string into name/value pairs and re-serializes it: event-handler names (on*) and tokens that are not shaped like an attribute are dropped, values are HTML-escaped (without double-encoding existing character references). This closes tag injection such as data-x=""><script>... and is linear in the input size.
Always on, independent of the policy: " in href and in the named attributes is escaped as ", since it is the only character that can end the double-quoted attribute value.
PimcoreCoreBundle::boot() installs the configured policy into the new Pimcore\Model\DataObject\Data\Link\SanitizerPolicy holder (and shutdown() resets it); an application can install its own via SanitizerPolicy::setInstance(), which is never overridden by the config. A small accessor AttributeSanitizer::rejectsEditorSuppliedAttributeKeys() was added for the attribute handling.
Backward compatibility
Checked against pimcore-backward-compatibility. Everything that removes existing behavior is behind strict, which defaults to false:
Default: the advisory is not closed. An editor can still store a javascript: link or an event-handler attribute until the site sets pimcore.objects.link_sanitizer.strict: true. While running on the permissive default, every render the strict policy would reject triggers Since pimcore/pimcore 2026.3: ... will be removed in 2027.1 (once per rejected href and once per rendering with rejected free-form attributes), where strict becomes the default. A policy installed explicitly via SanitizerPolicy::setInstance() opts out of the deprecation.
getHref(), getAttributes() and all other public getters/setters are unchanged, as are signatures, return types and stored data.
On the permissive default, getHtml() output is byte-identical to 2026.3 for every input that did not contain a " in href or in one of the named attributes (entities such as &, single-quoted or oddly spaced free-form attributes included). The only unconditional difference: a " in those values is rendered as " instead of breaking out of the attribute.
With strict: true the output intentionally changes for the inputs listed above: rejected URLs render href="", and free-form attributes are re-serialized (double quotes, normalized whitespace, dropped event handlers and tokens that do not parse as attributes, e.g. names outside [A-Za-z_:][-A-Za-z0-9_:.]* such as @click, or unquoted values containing =, quotes or angle brackets).
New: config keys pimcore.objects.link_sanitizer.{strict,blocked_url_schemes,block_unsafe_data_urls}, SanitizerPolicy, AttributeSanitizer::rejectsEditorSuppliedAttributeKeys(), PimcoreCoreBundle::boot()/shutdown() handling (the bundle is @internal), and a dependency of the DataObject Link on the Document AttributeSanitizer policy class. The Document Link setting is untouched.
Documented in doc/01_Documents/02_Templates/03_Editables/18_Link.md ("DataObject Link data type").
Testing
Tests: tests/Unit/Models/DataObject/Data/LinkSanitizerTest.php, plus config/boot coverage in tests/Unit/Bundle/CoreBundle/DependencyInjection/LinkSanitizerConfigurationTest.php and tests/Unit/Bundle/CoreBundle/PimcoreCoreBundleLinkSanitizerTest.php (independence from the Document setting, boot order, shutdown reset) — permissive default (byte-identical benign output, " escaping, deprecations, explicit-policy opt-out) and strict policy (unsafe href forms incl. obfuscation and data: URLs, event-handler stripping, tag injection, quoted values with </>/entities, long-whitespace input). The assertions were additionally run against the changed classes with the project autoloader outside PHPUnit; CI is the source of truth for the full suite.
…taObject Data\Link::getHtml
Link::getHtml() concatenated href and attribute values (title, class,
rel, target, tabindex, accesskey) plus the free-form attributes string
into the anchor tag without HTML-escaping, and never validated the
href scheme. A stored value could break out of an attribute to inject
event handlers, or use a javascript:/vbscript: URI to execute on
click.
Co-Authored-By: Claude <noreply@anthropic.com>
Verdict: Needs changes. The PR hardens DataObject link rendering against stored XSS.
Changes:
Escapes named anchor attributes and sanitizes executable URI schemes.
Filters event handlers from free-form attributes.
Adds regression tests for escaping and sanitization.
The output boundary is appropriate and covers getHtml()/__toString(), but raw attributes still permit tag injection at Link.php:458. The obfuscated-scheme test at LinkTest.php:236 also passes before the fix. Public signatures remain compatible; no documentation was changed.
The backward-compatibility claim that unaffected inputs remain byte-for-byte identical is incorrect: a normal URL such as https://example.test/?a=1&b=2 contains none of the listed dangerous input but now renders with &. Escaping is appropriate, but the PR description should distinguish equivalent rendered behavior from identical serialized output so the compatibility assessment is accurate.
This re-escapes entity references that are already valid markup in the free-form attribute string. For example, data-label="Tom " Jerry" becomes data-label="Tom &quot; Jerry", so the DOM value changes from Tom " Jerry to the literal text Tom " Jerry. Preserve recognized entities while still escaping raw metacharacters by disabling double encoding.
The new href escaping is not covered independently: the added attribute-escaping tests exercise only title and class, while the scheme tests contain no quote that could break out of href. Add a direct URL containing " and assert that it is encoded and cannot create a second attribute, so a regression on this security boundary is caught.
Whitespace recovery rescans long runs, causing render-time CPU spikes
models/DataObject/Data/Link.php:473
The recovery path advances to the whitespace itself, so a long whitespace run before an invalid token is rescanned once per byte. A self-contained benchmark of this exact loop grew from 0.9 ms at 1 KB to 16 ms at 8 KB; because this field is stored as TEXT and has no setter limit, crafted stored attributes can add substantial CPU cost to every render. Jump directly to the next non-whitespace token (or drop the remainder) instead.
… via the shared Link sanitizer policy
Keep getHtml() output unchanged by default (only escape double quotes) and apply the strict
AttributeSanitizer policy, as for the Document Link editable, when link_sanitizer.strict is enabled.
The permissive default triggers a deprecation for input the strict policy would reject.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nitizer config for the DataObject Link
The DataObject Link now has its own policy holder (SanitizerPolicy) and Symfony config,
independent of the Document Link editable setting.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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
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.
Advisory: GHSA-h78x-47qg-qjmq
Vulnerability
Pimcore\Model\DataObject\Data\Link::getHtml()(and__toString()) concatenatedhref, the named attributes (title,class,rel,target,tabindex,accesskey) and the free-formattributesstring directly into the<a>tag without escaping — only the link text went throughhtmlspecialchars(). The URI scheme ofhrefwas never validated either. Since this data type is populated from editor/end-user input, a stored value could:"inhref/title/class) to inject arbitrary markup or event handlers,javascript:URI so the link executes script on click,onclick=...) or break out of the tag via the free-formattributesstring.This is the DataObject sibling of GHSA-97cp-8873-v2gf (Document Link XSS) and uses the same mitigation approach as the Document Link editable fix in #19392 (GHSA-9g27-c28m-8xg5).
Fix
The mitigation is opt-in on this release line. It reuses the
Pimcore\Model\Document\Editable\Link\AttributeSanitizerpolicy class from #19392, but is enabled by its own, independent config (so DataObject links and Document links can be switched separately):With
strict: true,getHtml():hrefforjavascript:/vbscript:(and the configured schemes) and for script-capabledata:URLs (data:text/html,data:image/svg+xml, ...); whitespace/control-character and character-reference obfuscation (java\tscript:,javascript:) is covered by the shared policy,attributesstring into name/value pairs and re-serializes it: event-handler names (on*) and tokens that are not shaped like an attribute are dropped, values are HTML-escaped (without double-encoding existing character references). This closes tag injection such asdata-x=""><script>...and is linear in the input size.Always on, independent of the policy:
"inhrefand in the named attributes is escaped as", since it is the only character that can end the double-quoted attribute value.PimcoreCoreBundle::boot()installs the configured policy into the newPimcore\Model\DataObject\Data\Link\SanitizerPolicyholder (andshutdown()resets it); an application can install its own viaSanitizerPolicy::setInstance(), which is never overridden by the config. A small accessorAttributeSanitizer::rejectsEditorSuppliedAttributeKeys()was added for the attribute handling.Backward compatibility
Checked against
pimcore-backward-compatibility. Everything that removes existing behavior is behindstrict, which defaults tofalse:javascript:link or an event-handler attribute until the site setspimcore.objects.link_sanitizer.strict: true. While running on the permissive default, every render the strict policy would reject triggersSince pimcore/pimcore 2026.3: ... will be removed in 2027.1(once per rejectedhrefand once per rendering with rejected free-form attributes), where strict becomes the default. A policy installed explicitly viaSanitizerPolicy::setInstance()opts out of the deprecation.getHref(),getAttributes()and all other public getters/setters are unchanged, as are signatures, return types and stored data.getHtml()output is byte-identical to2026.3for every input that did not contain a"inhrefor in one of the named attributes (entities such as&, single-quoted or oddly spaced free-form attributes included). The only unconditional difference: a"in those values is rendered as"instead of breaking out of the attribute.strict: truethe output intentionally changes for the inputs listed above: rejected URLs renderhref="", and free-form attributes are re-serialized (double quotes, normalized whitespace, dropped event handlers and tokens that do not parse as attributes, e.g. names outside[A-Za-z_:][-A-Za-z0-9_:.]*such as@click, or unquoted values containing=, quotes or angle brackets).pimcore.objects.link_sanitizer.{strict,blocked_url_schemes,block_unsafe_data_urls},SanitizerPolicy,AttributeSanitizer::rejectsEditorSuppliedAttributeKeys(),PimcoreCoreBundle::boot()/shutdown()handling (the bundle is@internal), and a dependency of the DataObjectLinkon the DocumentAttributeSanitizerpolicy class. The Document Link setting is untouched.Documented in
doc/01_Documents/02_Templates/03_Editables/18_Link.md("DataObject Link data type").Testing
Tests:
tests/Unit/Models/DataObject/Data/LinkSanitizerTest.php, plus config/boot coverage intests/Unit/Bundle/CoreBundle/DependencyInjection/LinkSanitizerConfigurationTest.phpandtests/Unit/Bundle/CoreBundle/PimcoreCoreBundleLinkSanitizerTest.php(independence from the Document setting, boot order, shutdown reset) — permissive default (byte-identical benign output,"escaping, deprecations, explicit-policy opt-out) and strict policy (unsafehrefforms incl. obfuscation anddata:URLs, event-handler stripping, tag injection, quoted values with</>/entities, long-whitespace input). The assertions were additionally run against the changed classes with the project autoloader outside PHPUnit; CI is the source of truth for the full suite.Security-Advisory: pimcore/platform-version/GHSA-h78x-47qg-qjmq
Warning
Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
api.anthropic.comTo allow these domains, add them to the
network.allowedlist in your workflow frontmatter:See Network Configuration for more information.