XSS in footnote text

Created on 24 June 2025, 22 days ago

Problem/Motivation

The module creates a Footnote text format and uses it to restrict which tags are allowed in footnote text.
But it seems this text format is not really enforced when rendering the footnote text.
This could allow injecting arbitrary JS in the page.

Steps to reproduce

1. Create a text format with the footnotes filter.
2. In a CKE field using this format, use source editing to insert this HTML:

<p>
    <footnotes data-text="&lt;img src=x onload=&quot;alert()&quot; onerror=&quot;alert()&quot;&gt;" data-value="foo">&nbsp;</footnotes>
</p>

3. When the node is displayed, the JS is executed.

This can be mitigated by having the "Limit allowed HTML tags and correct faulty HTML" filter run after "Footnotes filter" in the node text format.

Proposed resolution

The module should filter the footnote text with the filters from the Footnote format when displaying the text, not just when saving the modal.

Remaining tasks

User interface changes

API changes

Data model changes

πŸ› Bug report
Status

Active

Version

4.0

Component

Footnotes

Created by

πŸ‡«πŸ‡·France prudloff Lille

Live updates comments and jobs are added and updated live.
  • Security

    It is used for security vulnerabilities which do not need a security advisory. For example, security issues in projects which do not have security advisory coverage, or forward-porting a change already disclosed in a security advisory. See Drupal’s security advisory policy for details. Be careful publicly disclosing security vulnerabilities! Use the β€œReport a security vulnerability” link in the project page’s sidebar. See how to report a security issue for details.

Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @prudloff
  • πŸ‡«πŸ‡·France prudloff Lille
  • First commit to issue fork.
  • Merge request !85Resolve #3532178 "Xss in footnote" β†’ (Merged) created by scott_euser
  • πŸ‡¬πŸ‡§United Kingdom scott_euser

    This should do it, check_markup applied (also to the value in case). The test coverage updates are mostly because that switches <b> tags to <strong> tags, but also covers stripping out an unordered list (which is not allowed in the 'footnote' text format) so that should protect against any future regression there.

  • πŸ‡¬πŸ‡§United Kingdom scott_euser
  • πŸ‡«πŸ‡·France prudloff Lille

    The check_markup() doc says:

    this function should only be used when filtering text for use elsewhere than on a rendered HTML page. If this is part of an HTML page, then a renderable array with a #type 'processed_text' element should be used instead of this

    So I think we should use something like this:

          'text' => [
            '#type' => 'processed_text',
            '#text' => $text,
            '#format' => 'footnote',
          ],
          'text_clean' => PlainTextOutput::renderFromHtml($text),
    

    I don't think the format needs to be applied to text_clean since PlainTextOutput::renderFromHtml() removes every tag anyway.

  • πŸ‡«πŸ‡·France prudloff Lille
  • πŸ‡¬πŸ‡§United Kingdom scott_euser

    Hi @prudloff,

    I think what that comment is suggesting though is actually what we are doing; ie, rendering for elsewhere other than the page. We do this to collapse multiple footnotes together + create the ref_id (which people could anchor link to). If we instead do not render it, we could change the behaviour of either. The code in check_markup is ultimately doing the same thing, just also triggering Renderer::renderInIsolation() to get the string version earlier on. Nervous to otherwise cause further side effects for a fairly well used functionality.

    Ie, we might change the behaviour of this bit

    if ($this->settings['footnotes_collapse']) {
      $key = $this->getMatchingFootnoteKey($text);
    }
    

    If we e.g. switch to comparing the text_clean version (which still then at least requires that to be rendered early).

    So I think its better to merge this and if you feel strongly that we should not render as early as we are currently doing, we can carry on in a follow-up.

    Thanks,
    Scott

  • πŸ‡«πŸ‡·France prudloff Lille

    Oh right, I didn't see that the module does a string comparison with the footnote text.

    The main problem with check_markup() is that it looses cache metadata or attached libraries generated by the rendered text (because it uses renderInIsolation()).
    So if one of the filters used in the Footnote text format adds cache metadata or attached libraries, this metadata will be lost.
    For example the "Caption images" filter adds the filter/caption CSS library to the page but if I enable this filter on the Footnote text format, this library is not added to the page.

    A solution would be to do the same thing as check_markup but with render() instead:

        $build = [
          '#type' => 'processed_text',
          '#text' => $text,
          '#format' => 'footnote',
        ];
        \Drupal::service('renderer')->render($build);
    

    This way cache metadata and assets correctly bubble to the page.

  • πŸ‡¬πŸ‡§United Kingdom scott_euser

    Makes sense; I updated the code. Rendering $value was breaking tests e.g. ::testCitationUniqueTextSameValue() but I can see its already running through Html::cleanCssIdentifier() if not is_numeric, so I believe that is safe. Did a tiny bit of scope creep to rename a variable to avoid confusion with the variable name being checked to auto-collapse multiple identical texts together.

  • πŸ‡«πŸ‡·France prudloff Lille

    I tested the MR and confirm it fixes the vulnerability.

  • Pipeline finished with Skipped
    7 days ago
    #543382
  • πŸ‡¬πŸ‡§United Kingdom scott_euser
  • πŸ‡¬πŸ‡§United Kingdom scott_euser

    Thank you!

Production build 0.71.5 2024