Skip to content

fix: the file list escapes the name it renders - #869

Merged
blaipr merged 1 commit into
mainfrom
fix/the-file-list-escapes-the-name-it-renders
Aug 23, 2026
Merged

fix: the file list escapes the name it renders#869
blaipr merged 1 commit into
mainfrom
fix/the-file-list-escapes-the-name-it-renders

Conversation

@blaipr

@blaipr blaipr commented Aug 23, 2026

Copy link
Copy Markdown
Member

An account's attachments are listed with the file name the browser supplied, and that
name is stored exactly as it arrived — deliberately, and the upload controller says so:
escaping on the way in stored the entities, so an attachment called "Q&A.txt" was saved
and downloaded as "Q&A.txt". The places that use it are each meant to handle it.
This one did not.

printf('%s (%d KB)', Html::truncate($file->getName() ?? '', 50), ...)

Html::truncate() truncates; it does not escape. The two title attributes on the lines
either side of it are correctly wrapped in $_e(). The response is PLAIN_TEXT and the
front end injects it with jQuery's .html(), so a file named with a script tag runs for
anyone who opens the account — and listing a file needs only view access, not edit, so
the reader is any of the people the account is shared with. On the same page a password
custom field's decrypted value sits in a data-pass attribute, which is what a payload
would go looking for.

The reason it survived is the more useful half of this change. ThemeEscapesWhatItRenders
looks for <?php echo, <?= and print, and its pattern requires whitespace after the
word — printf( has none, so every printf in every template was invisible to it. The
pattern now has a branch for printf, and switching it on found nine more places across
eight templates, all fixed here:

  • itemshow/item_preset-session_timeout.inc — an administrator's own IP preset
  • _partials/fixed-header.inc and _partials/footer.inc — the signed-in user's group name
  • install/index.inc and _layouts/main.inc — the application name and version
  • configManager/info.inc — the download rate
  • wiki/wikipage.inc, twice — page names and URLs (unrouted code, escaped anyway rather
    than left as the one exception)

None of those are as reachable as the first, but the point of the check is that it does
not depend on someone deciding which ones matter.

Checked by putting the truncate back unescaped: the guard fails, naming the file, the
line and the expression.

An account's attachments are listed with the file name the browser supplied, and that
name is stored exactly as it arrived — deliberately, and the upload controller says so:
escaping on the way in stored the entities, so an attachment called "Q&A.txt" was saved
and downloaded as "Q&amp;A.txt". The places that use it are each meant to handle it.
This one did not.

    printf('%s (%d KB)', Html::truncate($file->getName() ?? '', 50), ...)

Html::truncate() truncates; it does not escape. The two title attributes on the lines
either side of it are correctly wrapped in $_e(). The response is PLAIN_TEXT and the
front end injects it with jQuery's .html(), so a file named with a script tag runs for
anyone who opens the account — and listing a file needs only view access, not edit, so
the reader is any of the people the account is shared with. On the same page a password
custom field's decrypted value sits in a data-pass attribute, which is what a payload
would go looking for.

The reason it survived is the more useful half of this change. ThemeEscapesWhatItRenders
looks for `<?php echo`, `<?=` and `print`, and its pattern requires whitespace after the
word — `printf(` has none, so every printf in every template was invisible to it. The
pattern now has a branch for printf, and switching it on found nine more places across
eight templates, all fixed here:

  - itemshow/item_preset-session_timeout.inc — an administrator's own IP preset
  - _partials/fixed-header.inc and _partials/footer.inc — the signed-in user's group name
  - install/index.inc and _layouts/main.inc — the application name and version
  - configManager/info.inc — the download rate
  - wiki/wikipage.inc, twice — page names and URLs (unrouted code, escaped anyway rather
    than left as the one exception)

None of those are as reachable as the first, but the point of the check is that it does
not depend on someone deciding which ones matter.

Checked by putting the truncate back unescaped: the guard fails, naming the file, the
line and the expression.
@blaipr
blaipr merged commit 5b49287 into main Aug 23, 2026
8 checks passed
@blaipr
blaipr deleted the fix/the-file-list-escapes-the-name-it-renders branch August 23, 2026 23:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant