
Docker-based lab for reproducing and validating CVE-2026-28496, a Server-Side Template Injection vulnerability in FOSSBilling's Twig rendering, with vulnerable and patched comparison targets.
This repository contains a local Docker lab for reproducing and validating CVE-2026-28496, a Server-Side Template Injection vulnerability affecting FOSSBilling's Twig template rendering behavior.
FOSSBilling is a free and open-source billing and client management platform. Versions prior to 0.8.0 are affected by unsafe Twig template rendering behavior that can evaluate supplied template expressions. FOSSBilling 0.8.0 is used as the patched comparison target in this lab.
This lab compares two FOSSBilling versions:
| Service | FOSSBilling version | Purpose | URL |
|---|
| vuln | 0.7.2 | Vulnerable comparison target | http://localhost:8081 |
| patched | 0.8.0 | Patched comparison target | http://localhost:8082 |
The demonstrated HTTP validation path in this local lab is:
Unauthenticated HTTP request in this local FOSSBilling 0.7.2 lab
→ POST /api/system/system/string_render
→ JSON body contains _tpl={{ 7*7 }}
→ vulnerable target renders the Twig expression
→ patched target does not expose the same tested API behavior
In the vulnerable target, the API call returns:
{"result":"49","error":null}
In the patched target, the same request returns:
{"result":null,"error":{"message":"Unknown API call system/system/string_render","code":879}}
This lab validates the vulnerable-versus-patched HTTP behavior using FOSSBilling 0.7.2 and FOSSBilling 0.8.0.
The lab is intentionally scoped to local Docker services. It does not target external systems and does not include web shells, malware, persistence, external callbacks, database dumping, or destructive payloads.
| Claim | Evidence | How to verify in this lab |
|---|---|---|
| CVE-2026-28496 affects FOSSBilling versions prior to 0.8.0. | Public CVE and advisory metadata identify FOSSBilling prior to 0.8.0 as affected by Twig SSTI. | Review the References section and compare the vulnerable and patched target versions. |
| FOSSBilling 0.7.2 is used as the vulnerable comparison target. | The vulnerable service is built from the official fossbilling/fossbilling:0.7.2 Docker image. | Inspect vuln/Dockerfile and run docker compose ps -a. |
| FOSSBilling 0.8.0 is used as the patched comparison target. | The patched service is built from the official fossbilling/fossbilling:0.8.0 Docker image. | Inspect patched/Dockerfile and run docker compose ps -a. |
The vulnerable HTTP path is /api/system/system/string_render. | The vulnerable target returns JSON with result: "49" for _tpl={{ 7*7 }}. | Run python3 poc/poc.py --url http://localhost:8081. |
| The patched target does not expose the same HTTP behavior. | The patched target returns Unknown API call system/system/string_render. | Run python3 poc/poc.py --url http://localhost:8082. |
| The PoC is HTTP-only. | poc/poc.py sends HTTP POST requests and does not call Docker, Docker Compose, shell commands, or container APIs. | Inspect poc/poc.py. |
| The lab auto-installs both FOSSBilling targets during Docker Compose startup. | The one-shot installer sidecar containers complete setup and exit with status 0. | Run docker compose ps -a and docker compose logs installer-vuln installer-patched. |
| The vulnerable target renders the harmless Twig expression. | The HTTP response from port 8081 is {"result":"49","error":null}. | Run the vulnerable PoC command. |
| The patched target does not render the same expression through the tested API path. | The HTTP response from port 8082 is a JSON API error with code 879. | Run the patched PoC command. |
This lab uses FOSSBilling 0.7.2 as the vulnerable comparison target because public vulnerability research identifies FOSSBilling versions before 0.8.0 as affected, and 0.7.2 is the latest vulnerable release used in the tested chain.
This lab uses FOSSBilling 0.8.0 as the patched comparison target because public advisory metadata identifies 0.8.0 as the patched version.
This lab focuses on the observable HTTP behavior of:
POST /api/system/system/string_render
with this JSON body:
{"_tpl":"{{ 7*7 }}"}
The lab demonstrates that FOSSBilling 0.7.2 renders the supplied Twig expression through the HTTP API path, while FOSSBilling 0.8.0 does not expose the same API call.
This lab does not claim to test every FOSSBilling template rendering feature. CVE-2026-28496 also relates to other Twig rendering contexts, such as template rendering features available inside the application.
This lab does not demonstrate the full unauthenticated remote code execution chain. It validates the unauthenticated HTTP behavior observed in the local FOSSBilling 0.7.2 target and compares it with FOSSBilling 0.8.0. The full public chain involves additional API authorization behavior beyond the safe Twig expression validation shown here.
The lab does not demonstrate:
The root cause of CVE-2026-28496 is unsafe Twig template rendering.
FOSSBilling uses Twig to render dynamic templates. In vulnerable versions, a supplied template string can be passed into Twig rendering logic without sufficient sandbox restrictions.
The vulnerable behavior can be summarized as:
Input template string
→ FOSSBilling API receives _tpl
→ System\Api\Admin::string_render() reads _tpl
→ System\Service::renderString() receives the template string
→ Twig creates a template from the supplied string
→ Twig evaluates the expression
→ rendered output is returned in the HTTP response
For this harmless template expression:
{{ 7*7 }}
the vulnerable target evaluates the expression and returns:
49
The security issue is not limited to arithmetic evaluation. Arithmetic evaluation is only the safe visible signal used in this lab.
The more security-sensitive problem is that unsandboxed Twig templates may access objects and methods exposed in the template context. Public research describes a higher-impact path where Twig template execution can reach application internals, including the dependency injection container, when suitable template context objects are available.
The simplified vulnerable model is:
Template renderer
→ unsandboxed Twig expression
→ method/object access may be possible
→ application internals may become reachable
→ sensitive services may become reachable
The patched design in FOSSBilling 0.8.0 hardens the vulnerable behavior. In this lab, the patched target no longer exposes the tested API call:
{"result":null,"error":{"message":"Unknown API call system/system/string_render","code":879}}
The security lesson is:
Template engines must not render user-controlled template strings in a privileged application context unless strict authorization and sandbox boundaries are enforced.
The vulnerable HTTP behavior is backed by the source code path in FOSSBilling 0.7.2.
The API method receives _tpl from request data and passes it into the system service renderer.
Relevant vulnerable entry point:
public function string_render($data)
{
if (!isset($data['_tpl'])) {
error_log('_tpl parameter not passed');
return '';
}
$tpl = $data['_tpl'];
$try_render = $data['_try'] ?? false;
$vars = $data;
unset($vars['_tpl'], $vars['_try']);
return $this->getService()->renderString($tpl, $try_render, $vars);
}
The important data flow is:
HTTP request body
→ _tpl
→ System\Api\Admin::string_render()
→ System\Service::renderString()
In FOSSBilling 0.7.2, renderString() attempts to load the supplied value as a template name. If that fails, it treats the supplied value as a template string and passes it into createTemplateFromString().
Simplified vulnerable flow:
public function renderString($tpl, $try_render, $vars)
{
$twig = $this->di['twig'];
try {
$template = $twig->load($tpl);
$parsed = $template->render($vars);
} catch (\Exception) {
// $twig->load throws an exception when $tpl is a raw template string
$parsed = $this->createTemplateFromString($tpl, $try_render, $vars);
}
return $parsed;
}
The vulnerable sink is createTemplateFromString():
public function createTemplateFromString($tpl, $try_render, $vars)
{
try {
$twig = $this->di['twig'];
$template = $twig->createTemplate($tpl);
$parsed = $template->render($vars);
} catch (\Exception $e) {
$parsed = $tpl;
if (!$try_render) {
throw $e;
}
}
return $parsed;
}
The security-relevant source pattern is:
_tpl from request data
→ used as $tpl
→ passed to Twig createTemplate()
→ rendered server-side
This explains the vulnerable lab result:
POST /api/system/system/string_render
{"_tpl":"{{ 7*7 }}"}
Vulnerable response:
{"result":"49","error":null}
The value 49 proves that the supplied Twig expression was evaluated server-side.
The safe lab payload only uses arithmetic:
{{ 7*7 }}
However, the root cause is more security-sensitive than arithmetic expression evaluation. In vulnerable rendering contexts, Twig templates may interact with application objects that are present in the template environment. Public research describes higher-impact chains where API context objects can expose access to application internals such as the dependency injection container.
A source-level regression check confirmed the deeper behavior:
FOSSBilling 0.7.2:
{{ guest.getDi() }}
→ DI_VISIBLE
FOSSBilling 0.8.0:
{{ guest.getDi() }}
→ blocked by Twig sandbox policy
This is why the vulnerability is best understood as unsafe template rendering, not merely a calculator-style expression evaluation bug.
FOSSBilling 0.8.0 changes the vulnerable behavior by hardening string rendering and removing the tested vulnerable HTTP behavior.
In the patched version, string rendering is routed through sandbox-aware rendering rather than directly rendering arbitrary template strings with broad Twig capabilities.
The patched service code calls a sandbox-aware renderer:
$rendered = SandboxedStringRenderer::render(
$twig,
$tpl,
$vars,
$errorMessage
);
The sandboxed renderer creates and renders a template, but catches Twig sandbox violations and converts them into a controlled application error:
final class SandboxedStringRenderer
{
public static function render(
Environment $twig,
string $content,
array $context = [],
string $name = 'template'
): string {
try {
return $twig->createTemplate($content)->render($context);
} catch (SecurityError $e) {
throw new InformationException(
'%name% contains disallowed Twig syntax: %error%',
[
'%name%' => $name,
'%error%' => $e->getMessage(),
]
);
}
}
}
The sandbox policy blocks method and property access by default:
$methods = [];
$properties = [];
The security-relevant change is:
Before:
request-controlled template string
→ Twig createTemplate()
→ render without the patched sandbox boundary
After:
template string rendering
→ SandboxedStringRenderer
→ Twig sandbox policy
→ method/property access denied by default
For the public HTTP API path tested in this lab, FOSSBilling 0.8.0 does not expose the same vulnerable API call:
{"result":null,"error":{"message":"Unknown API call system/system/string_render","code":879}}
This gives two useful validation layers:
HTTP behavior validation:
0.7.2 renders {{ 7*7 }} through /api/system/system/string_render.
0.8.0 does not expose the same API behavior.
Source/root-cause validation:
0.7.2 allows unsafe Twig rendering behavior.
0.8.0 introduces sandboxed string rendering and blocks method/property access.
The lab keeps these two layers separate:
HTTP PoC result
proves the vulnerable endpoint behavior.
Source patch review
explains why unsafe Twig rendering was dangerous and how the patched version hardens it.
The lab runs two isolated FOSSBilling installations through Docker Compose.
.
├── docker-compose.yml
├── vuln/
│ └── Dockerfile
├── patched/
│ └── Dockerfile
├── poc/
│ └── poc.py
├── scripts/
│ └── auto-install.sh
├── README.md
└── .gitignore
The two FOSSBilling services use separate databases and separate application versions:
| Service | Component | Version / Role |
|---|---|---|
| vuln | FOSSBilling | vulnerable target application |
| patched | FOSSBilling | patched target application |
| vuln-db | MariaDB | database for vulnerable target |
| patched-db | MariaDB | database for patched target |
| installer-vuln | curl sidecar | auto-installs vulnerable target |
| installer-patched | curl sidecar | auto-installs patched target |
Default exposed services:
Vulnerable target: http://localhost:8081
Patched target: http://localhost:8082
The lab uses pinned FOSSBilling versions:
| Target | FOSSBilling version | Expected behavior |
|---|---|---|
| http://localhost:8081 | 0.7.2 | renders {{ 7*7 }} through the vulnerable API path |
| http://localhost:8082 | 0.8.0 | does not expose the same vulnerable API behavior |
The installer sidecars run automatically during docker compose up. They initialize both FOSSBilling targets with local disposable database credentials and then exit.
The lab does not create or modify the vulnerable API route.
The route /api/system/system/string_render is provided by the FOSSBilling application in the vulnerable 0.7.2 target after installation. The Docker lab only installs the application through its normal installer flow and then sends an HTTP request to the existing application endpoint.
The patched 0.8.0 target returns Unknown API call system/system/string_render, which confirms that the tested route behavior comes from the application version itself rather than from a lab-created route.
No Python third-party package is required. The PoC uses Python standard library modules only.
Start the lab from a clean state:
docker compose down -v --remove-orphans
docker compose up -d --build
Check service status:
docker compose ps -a
Expected running services:
cve-2026-28496-vuln
cve-2026-28496-patched
cve-2026-28496-vuln-db
cve-2026-28496-patched-db
Expected completed installer services:
cve-2026-28496-installer-vuln Exited (0)
cve-2026-28496-installer-patched Exited (0)
Check installer logs:
docker compose logs installer-vuln installer-patched
Check the web applications:
curl -i http://127.0.0.1:8081 | head
curl -i http://127.0.0.1:8082 | head
Run HTTP validation against the vulnerable target:
python3 poc/poc.py --url http://localhost:8081
Run HTTP validation against the patched target:
python3 poc/poc.py --url http://localhost:8082
The PoC accepts one local FOSSBilling base URL:
python3 poc/poc.py --url <target_url>
Examples:
python3 poc/poc.py --url http://localhost:8081
python3 poc/poc.py --url http://localhost:8082
python3 poc/poc.py --url http://127.0.0.1:8081
python3 poc/poc.py --url http://127.0.0.1:8082
The PoC sends this HTTP request:
POST /api/system/system/string_render
Content-Type: application/json
Request body:
{"_tpl":"{{ 7*7 }}"}
The PoC is HTTP-only. It does not call Docker, Docker Compose, shell commands, WP-CLI, or container APIs.
Command:
python3 poc/poc.py --url http://localhost:8081
Expected vulnerable target signal:
CVE-2026-28496 HTTP validation PoC
Scope: authorized local lab target only
URL: http://localhost:8081
Endpoint: http://localhost:8081/api/system/system/string_render
Template: {{ 7*7 }}
===== HTTP response =====
status=200
content-type=application/json; charset=utf-8
{"result":"49","error":null}
===== verdict =====
VULNERABLE/REACHABLE: server rendered {{ 7*7 }} and returned 49.
Command:
python3 poc/poc.py --url http://localhost:8082
Expected patched target signal:
CVE-2026-28496 HTTP validation PoC
Scope: authorized local lab target only
URL: http://localhost:8082
Endpoint: http://localhost:8082/api/system/system/string_render
Template: {{ 7*7 }}
===== HTTP response =====
status=400
content-type=application/json
{"result":null,"error":{"message":"Unknown API call system/system/string_render","code":879}}
===== verdict =====
PATCHED/NOT REACHABLE: target did not render the supplied template.
The important difference is:
FOSSBilling 0.7.2
→ renders {{ 7*7 }}
→ returns 49
FOSSBilling 0.8.0
→ does not render the supplied template through this API path
→ returns an API error
The validator sends a single HTTP POST request to the FOSSBilling API endpoint:
/api/system/system/string_render
The request body contains a harmless Twig expression:
{"_tpl":"{{ 7*7 }}"}
Expected vulnerable behavior:
HTTP 200 OK
JSON result is "49"
Expected patched behavior:
HTTP 400 Bad Request
JSON error indicates the API call is unknown or not reachable
This confirms that the vulnerable target evaluates the supplied template server-side.
The PoC intentionally uses {{ 7*7 }} instead of a destructive payload. The goal is to prove the technical condition safely:
attacker-controlled template input
+ server-side Twig evaluation
+ observable rendered output
For deeper source-level root-cause validation, method access is a stronger proof of the underlying issue. However, the public PoC in this repository uses the safer arithmetic expression to avoid demonstrating a high-impact chain.
Vulnerable probe:
curl -i -X POST \
'http://127.0.0.1:8081/api/system/system/string_render' \
-H 'Content-Type: application/json' \
--data '{"_tpl":"{{ 7*7 }}"}'
Expected result:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{"result":"49","error":null}
Patched probe:
curl -i -X POST \
'http://127.0.0.1:8082/api/system/system/string_render' \
-H 'Content-Type: application/json' \
--data '{"_tpl":"{{ 7*7 }}"}'
Expected result:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{"result":null,"error":{"message":"Unknown API call system/system/string_render","code":879}}
Server-Side Template Injection in a billing and client management platform is security-sensitive because the application may store customer records, billing data, server credentials, payment configuration, and administrator-controlled automation logic.
The demonstrated lab payload is harmless and only evaluates:
{{ 7*7 }}
However, the underlying class of vulnerability can be more serious when template execution has access to application objects, methods, or service containers.
Potential real-world impact, depending on configuration and reachable template context, may include:
This lab demonstrates only the safe HTTP validation signal. It does not demonstrate credential access, database access, extension installation, command execution, or post-exploitation.
Potential indicators include HTTP requests to the FOSSBilling API endpoint:
/api/system/system/string_render
Suspicious request pattern:
POST /api/system/system/string_render
Content-Type: application/json
Suspicious request body indicators:
_tpl
{{
}}
Twig syntax
Example access log pattern:
POST /api/system/system/string_render
Example JSON payload:
{"_tpl":"{{ 7*7 }}"}
Recommended monitoring actions:
/api/system/system/string_render._tpl in JSON request bodies.{{ and }}.High-signal detection idea:
POST request to /api/system/system/string_render
AND request body contains "_tpl"
AND request body contains "{{"
Another high-signal local validation artifact:
Request body:
{"_tpl":"{{ 7*7 }}"}
Response body:
{"result":"49","error":null}
Upgrade FOSSBilling to version 0.8.0 or later.
For production environments, update to the latest available release rather than stopping at the lab comparison version.
Recommended mitigation steps:
/api/system/system/string_render.Security engineering lessons:
Check container status:
docker compose ps -a
Check installer logs:
docker compose logs installer-vuln installer-patched
Check web services:
curl -i http://127.0.0.1:8081 | head
curl -i http://127.0.0.1:8082 | head
Run vulnerable HTTP validation:
python3 poc/poc.py --url http://localhost:8081
Run patched HTTP validation:
python3 poc/poc.py --url http://localhost:8082
Manual vulnerable request:
curl -i -X POST \
'http://127.0.0.1:8081/api/system/system/string_render' \
-H 'Content-Type: application/json' \
--data '{"_tpl":"{{ 7*7 }}"}'
Manual patched request:
curl -i -X POST \
'http://127.0.0.1:8082/api/system/system/string_render' \
-H 'Content-Type: application/json' \
--data '{"_tpl":"{{ 7*7 }}"}'
Inspect vulnerable source flow from the checked-out source tree:
git checkout 0.7.2
grep -n "function string_render" -A30 src/modules/System/Api/Admin.php
grep -n "function renderString" -A70 src/modules/System/Service.php
grep -n "function createTemplateFromString" -A30 src/modules/System/Service.php
Inspect patched sandbox renderer from the checked-out source tree:
git checkout 0.8.0
grep -R "SandboxedStringRenderer" -n src/modules src/library | head -30
grep -R "\$methods = \[\]\|\$properties = \[\]" -n src/library/FOSSBilling/Twig
Save validation evidence:
mkdir -p evidence
python3 poc/poc.py --url http://localhost:8081 \
| tee evidence/vulnerable-http-validation.txt
python3 poc/poc.py --url http://localhost:8082 \
| tee evidence/patched-http-validation.txt
docker compose ps -a \
| tee evidence/docker-compose-ps.txt
docker compose logs installer-vuln installer-patched \
| tee evidence/installer-logs.txt
Check FOSSBilling response headers:
curl -i http://127.0.0.1:8081 | grep -i 'x-fossbilling-version'
curl -i http://127.0.0.1:8082 | grep -i 'x-fossbilling-version'
Stop and remove containers and networks:
docker compose down --remove-orphans
Remove containers, networks, and volumes:
docker compose down -v --remove-orphans
Remove local evidence files if created:
rm -rf evidence/
This lab is for local security research and controlled demonstration only.
Do not run the PoC or manual curl requests against systems you do not own or do not have explicit authorization to test.
Do not use real production credentials, customer data, payment data, API keys, or production secrets in this lab.
The intended scope is limited to local Docker services such as:
http://localhost:8081
http://localhost:8082
http://127.0.0.1:8081
http://127.0.0.1:8082
The PoC is intentionally HTTP-only and local-scope. It does not call Docker, Docker Compose, shell commands, WP-CLI, or container APIs.
The lab does not include payloads for:
The goal is to demonstrate one specific technical condition in a controlled environment:
HTTP request
+ FOSSBilling string_render API path
+ Twig template expression
+ vulnerable target renders the expression
+ patched target does not render the expression
CVE Record: CVE-2026-28496 https://www.cve.org/CVERecord?id=CVE-2026-28496
GitHub Advisory: GHSA-57mv-jm88-66jc https://github.com/FOSSBilling/FOSSBilling/security/advisories/GHSA-57mv-jm88-66jc
VulnCheck: FOSSBilling Auth Bypass and Twig SSTI to Unauthenticated RCE https://www.vulncheck.com/blog/fossbilling-auth-bypass-ssti-rce
FOSSBilling Docker Documentation https://docs.fossbilling.org/getting-started/docker/
FOSSBilling GitHub Repository https://github.com/FOSSBilling/FOSSBilling
FOSSBilling Docker Image https://hub.docker.com/r/fossbilling/fossbilling
Twig Documentation: Sandbox Extension https://twig.symfony.com/doc/3.x/sandbox.html