Skip to content

[security] GHSA-h78x-47qg-qjmq: Escape output and sanitize href in DataObject Data\Link::getHtml - #19484

Merged
robertSt7 merged 11 commits into
2026.3from
security/ghsa-h78x-47qg-qjmq-dataobject-link-xss-74a092502bcdcba2
Oct 7, 2026
Merged

robertSt7 merged 11 commits into
2026.3from
security/ghsa-h78x-47qg-qjmq-dataobject-link-xss-74a092502bcdcba2

Conversation

@pimcore-deployments

@pimcore-deployments pimcore-deployments commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Advisory: GHSA-h78x-47qg-qjmq

Vulnerability

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 validated
            block_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&#58;) 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 &quot;, 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 &amp;, single-quoted or oddly spaced free-form attributes included). The only unconditional difference: a " in those values is rendered as &quot; 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.

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.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "api.anthropic.com"

See Network Configuration for more information.

Generated by Draft a security-advisory fix · claude · sonnet50 · 280 AIC · ⌖ 23.2 AIC · ⊞ 7.6K · ◷

…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>
@pimcore-deployments pimcore-deployments added ai-generated This content was generated using AI. Security labels Sep 25, 2026
@github-actions

Copy link
Copy Markdown

Review Checklist

  • Target branch (2026.2 for bug fixes, others 2026.x)
  • Tests (if it's testable code, there should be a test for it - get help)
  • Docs (every functionality needs to be documented, see here)
  • Migration incl. install.sql (e.g. if the database schema changes, ...)
  • Upgrade notes (deprecations, important information, migration hints, ...)
  • Label
  • Milestone

@robertSt7
robertSt7 marked this pull request as ready for review September 25, 2026 13:14
Copilot AI balanced review requested due to automatic review settings September 25, 2026 13:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Free-form attributes still permit arbitrary element injection, and one regression test does not verify its intended behavior.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity

Open (2)
What changed in this PR

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.

File Description
models/​DataObject/​Data/​Link.php Escapes and sanitizes generated anchor markup.
tests/​Model/​DataType/​LinkTest.php Adds security regression tests.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread models/DataObject/Data/Link.php Outdated
Comment thread tests/Model/DataType/LinkTest.php Outdated
@robertSt7 robertSt7 self-assigned this Oct 2, 2026
… attributes, tighten tests

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The sanitizer drops valid quoted values, double-encodes existing entities, and relies on an inaccurate compatibility assessment.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
Resolved since last review (2)
Previously missed (1)

In code that hasn't changed since last review

Low severity Correct inaccurate byte-for-byte compatibility claim

models/​DataObject/​Data/​Link.php:426

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 &amp;. Escaping is appropriate, but the PR description should distinguish equivalent rendered behavior from identical serialized output so the compatibility assessment is accurate.

Comment thread models/DataObject/Data/Link.php Outdated
…orm attribute values

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Free-form entities are double-encoded, the stated compatibility guarantee is inaccurate, and direct href escaping lacks regression coverage.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
Resolved since last review (1)
Previously missed (2)

In code that hasn't changed since last review

Medium severity Avoid double-encoding existing HTML entities

models/​DataObject/​Data/​Link.php:482

This re-escapes entity references that are already valid markup in the free-form attribute string. For example, data-label="Tom &quot; Jerry" becomes data-label="Tom &amp;quot; Jerry", so the DOM value changes from Tom " Jerry to the literal text Tom &quot; Jerry. Preserve recognized entities while still escaping raw metacharacters by disabling double encoding.

Low severity Add independent href quote-escaping regression coverage

models/​DataObject/​Data/​Link.php:426

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.

Comment thread models/DataObject/Data/Link.php
…e-form attributes, cover href escaping

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…47qg-qjmq-dataobject-link-xss-74a092502bcdcba2

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The malformed-attribute recovery loop can impose substantial CPU cost on rendering crafted stored values.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)
Previously missed (1)

In code that hasn't changed since last review

Medium severity 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.

Comment thread models/DataObject/Data/Link.php Outdated
…step to avoid quadratic rescans

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Script-capable data URLs remain allowed, and the backward-compatibility assessment does not match the implemented normalization.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity · 1 Low severity

Open (3)

Comment thread models/DataObject/Data/Link.php Outdated
… to also reject script-capable data: URLs

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The implementation contradicts its stated backward-compatibility basis by altering benign serialized HTML output.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)
Resolved since last review (1)

… and named attributes

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… 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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The broader output compatibility change must be accurately assessed and documented before approval.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)

…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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Security-sensitive HTML parsing requires maintainer validation, and the duplicated configuration schema remains unresolved.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)
Resolved since last review (1)

Comment thread bundles/CoreBundle/src/DependencyInjection/Configuration.php Outdated
…between documents and objects

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The security-critical URL and HTML parsing behavior warrants final maintainer security validation.

Review effort: Balanced
Findings: None

Resolved since last review (2)

@robertSt7 robertSt7 added this to the 2026.3.2 milestone Oct 7, 2026
@robertSt7
robertSt7 merged commit 335e12b into 2026.3 Oct 7, 2026
23 checks passed
@robertSt7
robertSt7 deleted the security/ghsa-h78x-47qg-qjmq-dataobject-link-xss-74a092502bcdcba2 branch October 7, 2026 11:08
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 7, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

ai-generated This content was generated using AI. Security

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants