Security Hall of Fame

Valid, previously-unknown reports under our bug bounty program. Summaries are published after a fix, while a confirmed issue is still open, or when we have decided not to change the behaviour. We do not disclose tokens, personal data or exploit steps.

Bug bounty program — scope, rules and rewardsOther reports — features, non-vulnerabilities, and known issues with no fix yet

27 findings

Security Hall of Fame
Finding
Critical9.0

wcenter / djangoeurope API

Ade Aulia
High8.8

wcenter / djangoeurope API

Ade Aulia
High7.7

wcenter / djangoeurope API

Ade Aulia
High7.5

djangoeurope message queue

Anupam Giri
Medium6.8

wcenter / djangoeurope

Naveed Qadir
Medium6.8

wcenter / djangoeurope API

Ruruho
Medium6.8

wcenter / djangoeurope API

Fauzi Susanto
Medium6.8

wcenter / djangoeurope

Pramod Kumar Ravela
Medium6.6

djangoeurope message queue

Anupam Giri
Medium6.5

wcenter / djangoeurope API

Ade Aulia
Medium6.5

wcenter / djangoeurope API

Rey
Medium6.5

djangoeurope DNS

Ade AuliaWon't fix
Medium6.4

djangoeurope registration & password reset

Gaurang Maheta
Medium5.9

wcenter / djangoeurope API

Ruruho
Medium5.4

wcenter / djangoeurope API

Fauzi Susanto
Medium5.4

wcenter / djangoeurope API

Riski Rumata
Medium5.3

wcenter / djangoeurope API

Danu Fandiyana
Medium5.3

djangoeurope registration, password reset, trial accounts and contact form

Gaurang Maheta
Medium5.3

djangoeurope registration

Gaurang Maheta
Low4.3

wcenter / djangoeurope API

Ruruho
Low3.7

djangoeurope profile

Anupam Giri
Low3.7

wcenter / djangoeurope API

Peter Levashov
Low3.6

wcenter / djangoeurope API

Fauzi Susanto
Low3.3

wcenter / djangoeurope SSH hosting

Fauzi SusantoNot fixed yet
Low3.1

wcenter / djangoeurope payments

Anupam Giri
Low2.2

wcenter / djangoeurope API

Danu Fandiyana
Low0.0

wcenter / djangoeurope API

Raj Chaudhary

Want to be listed? Report a finding through the bug bounty program.

Finding

Cross-tenant home-directory takeover via PATCH /systemuser/

CriticalFixedwcenter / djangoeurope API

Ade Aulia · X

CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H = 9.0, Critical

CWE-915 · CWE-639 · CWE-22 — CWE-915 (Improperly Controlled Modification of Dynamically-Determined Object Attributes) covers the discarded read-only list. CWE-639 (Authorization Bypass Through User-Controlled Key) covers uid and home not being bound to the owning account. CWE-22 (Path Traversal) covers the agent and scanner consuming the stored home path.

PATCH /systemuser/<id>/ accepted uid and home from the request body and stored them. SystemUserSerializer declared its read-only tuple as read_only_field, missing the “s”. DRF reads read_only_fields and does not validate unknown Meta options, so the tuple was discarded and uid and home stayed writable. A root provisioning agent then consumed both: it changed ownership of the home directory to the stored uid, wrote authorized_keys inside the stored home as root, and on deletion removed the directory recorded in /etc/passwd. Nothing tied either field to the account that owned the record. The malware scanner built its path from home, with no containment check. POST was safe: it uses a different serializer that spelled the option correctly.

Confirmed. Classified as Critical. S:C because the impact lands as root on a separate worker host and crosses the tenant boundary into another customer’s files — two different security authorities. AC:H because an attacker needs the victim to be on the same host and needs their home path. C:H/I:H/A:H: ownership takeover, a root-written key file, and recursive deletion of another account’s home. The spelling is corrected, so uid, home, name and the rest are read-only on update again. A field that is both declared on the class and listed in read_only_fields stayed writable even with the correct spelling, because DRF applies that option only to generated fields; the shared base serializer now forces read-only for every listed name. That also closed a sibling hole: a customer could raise their own system group’s disk and RAM limits past their plan. A project-wide pass found no other misspelling; the other declared-and-listed cases were checked. Follow-ups: the agent should derive home from the allocated identity, and the scanner path should stay inside the account’s own directory. The fix went to production the same day; a follow-up hardening went out in 2.38.1.

Command injection in POST /systemuser/<id>/recovery/

HighFixedwcenter / djangoeurope API

Ade Aulia · X

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H = 8.8, High

CWE-78 · CWE-88 — CWE-78 (OS Command Injection) covers dst being expanded by the node’s login shell. CWE-88 (Argument Injection) covers a client path being read as an rsync option or operand.

POST /systemuser/<id>/recovery/ took the dst path and the backup agent passed it to rsync as a remote operand. rsync runs the remote side through the node’s login shell, so metacharacters in dst were expanded and executed on the node as root. Execution was on the node the system user is assigned to, not on the backup host. This is its own defect: a shell string built from a client-supplied path. The rejection of .. is a separate control.

Confirmed. Classified as High. S:U because the panel, the backup agent and the nodes are one administrative authority. The node’s egress firewall rejected follow-on outbound connections, so a second stage or an interactive shell could not be opened. That is an environmental factor and does not change the base vector. Fixed. The recovery, scan and backup path validator now rejects anything outside a strict character set: no shell metacharacters, no .., and no leading hyphen. The destination stays inside the account’s own home. Released as 2.38.10. The backup worker validates every operand and passes paths to rsync as arguments, with --protect-args and a -- separator. scan.path uses the same validator, so it no longer reaches a shell.

Cross-tenant RabbitMQ credential attachment

HighFixeddjangoeurope message queue

Anupam Giri · LinkedIn

CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H = 7.5, High

CWE-639 · CWE-862 — CWE-639 (Authorization Bypass Through User-Controlled Key / IDOR). CWE-862 (Missing Authorization) is the missing ownership check on the vhost.

POST /rabbitmquser/ did not check that the caller owned the vhost named in the request body, so any authenticated account could attach a credential of its own choosing to any vhost whose UUID it knew.

Confirmed, and accepted at High. We take the reporter’s word on the AMQPS step and did not need to re-run it. We debated Medium internally, on the grounds that an attacker needs a vhost UUID and the report does not demonstrate a way to obtain one. That second point is fair, and we checked it ourselves rather than assume: the id of the RabbitMQ instance is a UUID4 id. A UUID4 is 122 bits of random space, so guessing or enumerating one is impractical, and taking over another customer’s RabbitMQ account this way is very hard. The one broadly readable RabbitMQ endpoint exposes clusters without their vhosts, so we found no disclosure path either. Fixed: attaching credentials now requires that the caller owns the vhost.

Signup confirmation activates an attacker-chosen password

MediumFixedwcenter / djangoeurope

Naveed Qadir · LinkedIn

CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N = 6.8, Medium

CWE-287 · CWE-640 — CWE-287 (Improper Authentication) covers activating an account under a password chosen before the inbox owner confirmed it. CWE-640 (Weak Password Recovery Mechanism for Forgotten Password) covers two-factor authentication that still blocked login after a successful password reset.

An address that is not yet a customer can be registered first and left unconfirmed, with a password chosen at that registration. When the real owner later tries to sign up with the same address, the page says a confirmation link was sent. No mail is delivered, and there is no way to ask for another link. The only live link is the one from the first registration. Opening it activates the pending account under the password set then. Two-factor authentication enrolled on that account still blocks login after the owner completes a password reset. An address that is already an active account is unaffected: that signup notifies the owner and does not create a second pending account. The chain pre-hijacks a future signup.

Confirmed. Classified as Medium. AC:H because the attacker must know in advance that a specific person intends to register, and must register that exact address first. UI:R because that person must then open the confirmation link. C:H/I:H when the chain completes, because the account controls the customer’s hosted environment; A:N. Anonymous signup now stores a password that cannot be used to log in. Opening the confirmation link is what sets the password and activates the account, so the person who controls the inbox chooses it. Registering an address that is already active does not change that account’s password. Creating a reseller or sub-account while signed in still sets a password. Follow-ups, not required to close this finding: notify the address when two-factor authentication is enrolled or changed, and give someone who has proved they own the inbox a recovery path past two-factor authentication; a resend option that answers the same way for every address and is rate-limited; release an address that stays unverified after its confirmation window.

Delegated access survives a password reset

MediumFixedwcenter / djangoeurope API

Ruruho · GitHub

CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N = 6.8, Medium

CWE-613 — CWE-613 (Insufficient Session Expiration) covers a delegated-access grant remaining valid after the owner completes account recovery.

A completed password reset ended the account’s interactive sessions but left delegated switch-user access intact. A delegate kept full control, including creating an SSH key on the owner’s account and replacing a database password. Delegation is a normal product feature and cannot be created for an account the caller does not already hold. Nothing in the reset flow, the confirmation mail, or the account UI showed that the grant was still active.

Confirmed. Classified as Medium. AC:H because it needs a grant that already exists; the realistic case is an offboarded contractor. The reset page and the change-password page now list every account that can access this account, each with a checkbox that stays checked by default. Unticking an account and submitting the new password removes the grant and resets that delegate’s switched tokens. The confirmation mail lists what still has access and what was revoked.

Password change does not revoke existing sessions

MediumFixedwcenter / djangoeurope API

Fauzi Susanto · LinkedIn

CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N = 6.8, Medium

CWE-613 — CWE-613 (Insufficient Session Expiration) covers a session token remaining valid after the password change.

PATCH /user/<id>/ set a new password but left session tokens issued before the change valid for the rest of their lifetime. A token captured before the change still returned the account. The old password no longer worked, so the credential change was real; only session revocation was missing. The API reports a seven-day token lifetime. A stolen session therefore kept full account access after the owner changed their password, the usual remedy. Panel accounts can hold SSH keys, system users, domains and payment instruments.

Confirmed. Classified as Medium. It gives full account access, but only to someone who already holds a valid session token, so it is a persistence weakness rather than an independent entry point; hence AC:H. Password reset already revoked browser sessions; the authenticated password change did not. A successful password change now revokes every browser session for the account except the one making the change. User-created API keys are kept, as on a password reset, so automation keeps working; the owner can rotate those separately. A session created by browser login no longer outlives the password change. Released as 2.38.8.

Password reset token not invalidated after use

MediumFixedwcenter / djangoeurope

Pramod Kumar Ravela · India · LinkedIn · Bugcrowd

CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N = 6.8, Medium — top of the band, just under High.

CWE-640 · CWE-613 — CWE-640 (Weak Password Recovery Mechanism for Forgotten Password). CWE-613 (Insufficient Session Expiration) covers the token remaining valid after a successful change.

Reported that a password reset token was not invalidated after a successful password change, allowing a reset link to be replayed for the remainder of its two-hour lifetime.

Reset tokens are now strictly single-use. The report also prompted a wider review of our password reset flow, which led us to enforce our password policy on that path, sign out existing browser sessions on reset, invalidate superseded reset links, and add rate limiting to the endpoint. Our thanks for a clearly written and reproducible report.

Deleted RabbitMQ credentials remain live at the broker

MediumFixeddjangoeurope message queue

Anupam Giri · LinkedIn

CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H = 6.6, Medium

CWE-459 · CWE-613 — CWE-459 (Incomplete Cleanup). CWE-613 (Insufficient Session Expiration) covers deleted AMQP logins remaining valid at the broker.

DELETE /rabbitmquser/ removed the credential from the control panel without revoking it at the RabbitMQ broker, so a deleted AMQP login could keep working.

Holds. Classified as Medium. Deleting a credential in the panel removed the database row and sent nothing to the broker. The broker only dropped logins missing from a full user-list save, so a deleted AMQP login stayed live until someone else on the same cluster saved a credential — on a quiet cluster that can take a long time. PR:H because the attacker must already have held a working credential for that exact vhost; the flaw is that revocation did not take effect, not that anyone gained access they never had. AC:H because the next full sync of the cluster removes it, a condition the attacker does not control. While it lasts, the credential has full read, write and delete on the vhost. A different root cause from the first finding, so not a duplicate. A later vhost-level follow-up is the same root cause and the same fix, and is not scored separately. A delete now syncs the cluster user list, so the login is revoked at the broker.

Billable resources are provisioned before payment

MediumFixedwcenter / djangoeurope API

Ade Aulia · X

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L = 6.5, Medium

CWE-840 · CWE-862 — CWE-840 (Business Logic Errors) covers provisioning the account before the billing state allows it. CWE-862 (Missing Authorization) covers the missing gate on payment.

Creating a system user for a product still waiting for activation produced a working shell and live MySQL and PostgreSQL logins at once. The billing record stayed honest: inactive, unpaid, and read-only, with the status “Waiting for activation”. The only thing that later disabled the account was the billing suspension sweep, a window of about 15 to 20 minutes. The impact is unpaid use of production infrastructure. It does not reach other customers’ data and it is not local root.

Confirmed. Classified as Medium. A system user whose product is still waiting for activation is now created disabled on the node, with a shell that cannot log in, and becomes usable only when the product is paid. Released as 2.38.15. The free trial-account form stays off unless it is explicitly switched on. MySQL and PostgreSQL logins are created only once the product is paid (2.38.16). The same withholding covers cancelled and overdue accounts, so their database logins are removed too (2.38.17).

Known password moves the login address

MediumFixedwcenter / djangoeurope API

Rey · X

CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:H/A:H = 6.5, Medium

CWE-640 · CWE-287 — CWE-640 (Weak Password Recovery Mechanism for Forgotten Password) covers password-reset mail following the rewritten login address. CWE-287 (Improper Authentication) covers an address change that asked for the password and not for a code from an active MFA device.

With the account password, two routes rewrote the login address in place. PATCH /user/<id>/ with a new username did it until 2.39.0. PATCH /email/<id>/ on the login address still did it: the row’s address became the new one at once, the username followed, and the validation mail went to that new address. The previous address was dropped from the account and was not told. Password-reset mail goes only to the account’s validated addresses, so once the new address was validated every reset went there alone. An active MFA device still stopped a later login: the reset completed, the device stayed, and login asked for the code. Sessions and API keys stay valid across an address change on purpose.

Confirmed. Classified as Medium. PR:H because the attacker needs the account password and a session, on an account with no active MFA device. C:N because that password already allows reading the account. I:H because the change takes the address password resets are sent to. A:H because the holder can no longer recover the account through that address and is not told. Fixed in 2.46.0. Before the login address is replaced, removed, or left behind, a validated address is mailed. The mail says where the request came from and asks the holder to contact us if they did not make the change. An address that was never validated is not mailed, because it may be a typo. Adding or changing an e-mail address, saving the user form, and deleting the account now also ask an MFA user for a code after the password. The password is checked first, so a wrong password does not use up a code. A code sent to an e-mail device goes to the account’s own addresses, at most once a minute, and cannot be used to log in or to set up a device. Accounts without MFA are unchanged. Most accounts are asked, because new accounts have e-mail two-factor authentication by default since 2.38.25. A script that changes an e-mail address, saves the user form, or deletes an MFA account now sends the code. Sessions and API keys are still kept: ending them would sign out the holder, who is not the one making the change, and would break customer automation. The mail to the previous address is what tells the holder. A separate issue, informational: once an address was already validated, opening its confirmation link succeeded for any code and named the address, for whoever held that link. It changed no state and is not part of this chain. The link now answers only that the address is already confirmed, and the code is rotated when the address is validated. Follow-ups: PATCH /email/ on the login address still points the username at the new address before that address is validated, unlike PATCH /user/ since 2.39.0. DELETE /email/<id>/ still needs no password. Setting up a new MFA device still needs only the password.

A DNS zone can be created before the domain is delegated

MediumWon't fixdjangoeurope DNS

Ade Aulia · X

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N = 6.5, Medium

CWE-284 — CWE-284 (Improper Access Control) covers creating a zone for a name the account does not own.

An account can create a DNS zone, and records in it, for a name it does not own. The zone is served by our authoritative nameservers. A resolver uses that answer only after the domain’s registrar delegates the name to ns1.wservices.ch, ns2.wservices.ch and ns3.wservices.ch. Until then the zone changes nothing for a domain delegated elsewhere.

Confirmed. Classified as Medium. PR:L because it takes an account. C:N. I:H because a zone that is actually delegated is served as that name’s DNS. A:N. We are not adding an ownership check. Creating the zone first, and pointing the registrar at our nameservers afterwards, is the normal hosting workflow. A TXT challenge or a WHOIS email before a zone may exist would break that order. We accept the trade-off. One edge is real: if the registrar already points at our nameservers and the zone here is gone, for example a closed account that left the delegation in place, someone else could create the zone and it would be served. We are evaluating a check for that case that leaves the normal pre-delegation flow alone. Since 21 Oct 2026, creating a domain needs an active hosting product or a real payment, so a throwaway account is not enough. That is not proof of ownership. A punycode lookalike is not a collision with an existing zone and not a hijack. Checking the decoded label against IDNA2008 is low-priority hardening, separate from this decision.

Missing rate limits on registration and password reset

MediumFixeddjangoeurope registration & password reset

Gaurang Maheta

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N = 6.4

CWE-770 · CWE-799 — CWE-770 (Allocation of Resources Without Limits or Throttling). CWE-799 (Improper Control of Interaction Frequency) is the frequency-control view of the same gap.

Login is throttled, but registration and password-reset were not. Registration could create accounts and send welcome mail without a cap; password reset sent mail on every request, so one inbox could be flooded.

Accepted at Medium. The reported vector is unchanged. The harm is mail volume and sending reputation, which CVSS models poorly because the impact falls on a third party’s inbox rather than on our own system; that does not change the rating. The fix is deployed. Registration cannot bomb a single address — one welcome mail per address — but could send to many distinct addresses; password reset could flood one inbox. Both endpoints now have rate limits, including a per-target cap on reset mail so rotating IPs against one inbox no longer works. Reset tokens remain strictly single-use, independently of those limits. Our thanks for a clear and reproducible report; it prompted us to review the whole unauthenticated surface around registration and password reset.

GET /token/ returned the plaintext API key

MediumFixedwcenter / djangoeurope API

Ruruho · GitHub

CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:L/A:N = 5.9, Medium

CWE-312 · CWE-522 — CWE-312 (Cleartext Storage of Sensitive Information) covers returning the secret key after creation. CWE-522 (Insufficiently Protected Credentials) covers that key remaining usable as the account.

API tokens stay valid across a password reset on purpose, so customer automation does not break. GET /token/ returned the plaintext key on later reads, so knowing the password was enough to read the surviving non-expiring keys and keep using them. The token authenticates as the account, so it could change hosting objects.

Confirmed. Classified as Medium. The secret key is shown once, in the creation response, and is masked on every later read. A default lifetime, and a way for a token to revoke itself, remain under consideration because they affect existing automation.

A system user’s own resource limits overrode the paid plan

MediumFixedwcenter / djangoeurope API

Fauzi Susanto · LinkedIn

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L = 5.4, Medium

CWE-602 · CWE-770 — CWE-602 (Client-Side Enforcement of Server-Side Security) covers limits that only the panels’ sliders kept within the plan. CWE-770 (Allocation of Resources Without Limits or Throttling) covers the resources it handed out.

POST /systemuser/ and PATCH /systemuser/<id>/ took a system user’s RAM, disk space and process limits from the request. They were checked only against fixed bounds, or not at all when a user was added to an existing group. The panels’ sliders stop at the group’s plan. The API did not. The server applied the user’s own values. On an X12 plan, 2 GB of RAM, 400 processes and 50 GB, one user received a memory limit of 16 GiB, the agent’s ceiling, 2050 processes and a 500 GB quota.

Confirmed. Classified as Medium. PR:L because it takes an account with a system group. C:N. I:L because the plan does not pay for those resources. A:L because on a shared server the memory and processes are taken from other customers. Fixed in 2.40.2. The API refuses a user limit above the group’s, and the server never gets more than the group’s limits, also for values stored earlier. 2.46.4 extended this to moving a user to another group and to a smaller plan, and checks the number of users on both.

Account endpoints ignored the API-key permission matrix

MediumFixedwcenter / djangoeurope API

Riski Rumata

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N = 5.4, Medium

CWE-863 — CWE-863 (Incorrect Authorization) covers an ownership check that ignored the key’s permission matrix.

The per-key permission matrix was applied only to infrastructure resources. Account-level endpoints used an ownership check and never read the matrix, so a key created with every permission set to false could still add an SSH key, read the profile, email addresses and the MFA device list, list ticket categories, and open tickets. The ticket toggle was offered in the matrix and had no effect. OTP secrets were not returned. Everything reached belonged to the key owner’s own account.

Confirmed. Classified as Medium. Confidentiality is low because a restricted key reached only its owner’s own account. The integrity impact is that a scoped-down key could still plant an SSH key and open tickets. A key that carries a permission matrix is now deny-by-default on every endpoint, enforced once per request. It reaches a resource only when the matrix grants the action, so adding an SSH key needs sshkey/add, and reading the profile needs profile/view. sshkey, profile, email, otpdevice, ticketcategory and ticket are part of the matrix, and the ticket toggle now applies. A key with no restrictions, and the browser session, are unchanged. The panel has a permission editor for the matrix.

Client-side-only registration anti-bot check

MediumFixeddjangoeurope registration, password reset, trial accounts and contact form

Gaurang Maheta

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N = 5.3, Medium

CWE-602 · CWE-804 — CWE-602 (Client-Side Enforcement of Server-Side Security). CWE-804 (Guessable CAPTCHA) is the more precise fit and is cited alongside it.

The registration anti-spam check could be computed entirely in the client — no server secret, nonce or expiry — so it did not stop automated account creation.

Accepted in full. Registration, password reset, trial-account creation and the contact form now require a server-issued proof-of-work challenge bound to the request: signed and scoped by our own infrastructure, with an expiry, single-use, and not forgeable offline. We use self-hosted proof-of-work rather than a third-party CAPTCHA, so there is no extra processor, cookie or fingerprinting. Proof-of-work is not a hard barrier; rate limits remain the main ceiling on abuse. The old client-side gate is being removed in a staged change once this has been stable. Our thanks for a clean, correct report that got a long-standing issue fixed.

Unauthenticated customer-email enumeration

MediumFixeddjangoeurope registration

Gaurang Maheta

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N = 5.3

CWE-204 — CWE-204 (Observable Response Discrepancy).

The registration endpoint returned different error messages for already-registered versus unknown email addresses, so an unauthenticated caller could confirm which addresses were customers.

The oracle is closed. Anonymous registration now answers the same way whether the address is new or already a customer; a duplicate creates nothing. The address owner may receive a notice that an account already exists, linking to the password-reset page with no reset token. Our thanks for reporting this alongside the rate-limit finding.

A zero-cost product unlocked domain creation

LowFixedwcenter / djangoeurope API

Ruruho · GitHub

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N = 4.3, Low

CWE-863 — CWE-863 (Incorrect Authorization) covers accepting any active product where a hosting product was required.

Domain creation checked only that some active product existed, not that it was a hosting product. A settled zero-cost RabbitMQ vhost plan unlocked domain objects, DNS records and mail-forwarding rules for a name the account did not own. The created zones were not served. Websites, databases and mailboxes still required an activated hosting system user.

Confirmed. Classified as Low. The gate now requires an active SSH group or system user, or a genuine payment with a real payment method recorded. A zero-value settlement no longer counts.

Unauthenticated profile photos with EXIF location data

LowFixeddjangoeurope profile

Anupam Giri · LinkedIn

CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N = 3.7, Low

CWE-552 · CWE-212 · CWE-459 — CWE-552 (Files or Directories Accessible to External Parties). CWE-212 (Improper Removal of Sensitive Information Before Storage or Transfer) covers EXIF location left on the file. CWE-459 (Incomplete Cleanup) covers the file remaining after the profile photo was cleared.

Profile photos were stored under the original filename and served without a login. EXIF location data was left on the file. Clearing a photo from the profile did not delete it, so the file stayed reachable.

Fixed. Classified as Low. There was no profile-picture upload form in the front-end, so reaching the upload path required more than using the product as shipped (AC:H). Photos are stored under a non-guessable name, EXIF location data is stripped on upload, and removing a photo from the profile deletes the stored file.

Owner cannot revoke scoped API keys

LowFixedwcenter / djangoeurope API

Peter Levashov · SEVERADAO SECURITY LLC

CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N = 3.7, Low

CWE-863 · CWE-284 — CWE-863 (Incorrect Authorization) covers checking the target key’s capabilities instead of the caller’s. CWE-284 (Improper Access Control) covers the token-lifecycle gap, including the expiry PATCH that ignored stored permissions.

DELETE and PATCH /token/<id>/ checked whether the key being changed could manage API keys, instead of whether the caller could. A key with a permissions object never has that flag, so the owner was refused for every scoped key. Only full-access keys could be changed or deleted. The keys remained usable after those attempts. Impact stayed within the key’s assigned scope: the owner could not terminate a compromised scoped credential.

Confirmed. Classified as Low. AC:H because the attacker must already hold a key leaked by some other route; the bug only kept open a leak that the owner could not close themselves. Support could still delete any key on request. A second defect sat behind the 403: even with that check corrected, a PATCH carrying only a new expiry would have been refused with "Permission option required.", because the stored permissions were not taken into account. Both are fixed. The owner can now change and delete every key of theirs, whatever the key itself may do. Scoped keys still cannot manage keys. Regression tests cover scoped and full-access keys, with and without an expiry, can_manage_tokens true and false, and the reverse direction (a scoped key cannot delete, widen or create keys). API keys are kept alive on a password reset on purpose, so a routine forgot-password does not break customer automation; the password-changed mail lists the remaining keys and asks the customer to delete any they do not recognise. For scoped keys that deletion was the step this finding showed to be broken. The fix goes out with the next release.

The backup list included snapshots from before the user

LowFixedwcenter / djangoeurope API

Fauzi Susanto · LinkedIn

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N/CR:L = 3.6, Low

CWE-200 — CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor) covers listing backup runs and older snapshots the account did not need.

GET /backup/ returned every backup of the caller’s shared server. A server-wide snapshot is the restore point for a customer’s own home, so those rows belong on the account. The list also included snapshots taken before the caller’s users existed on that server, and runs that did not finish. The list itself does not open a backup.

Confirmed. Classified as Low. PR:L because it takes an account on that server. C:L is the extra rows: runs that did not finish, and snapshots from before the account’s users were on the server. The account had no use for them. Backup times and run states are worth little on their own, so the confidentiality requirement is low. The base score is 4.3 and the environmental score is 3.6. I:N and A:N. The server in the list is the customer’s own. Fixed in 2.46.4. GET /backup/ now lists what that account can open: the user’s own backups, and server-wide snapshots taken since the user came to the server. A snapshot from before then is not listed. A run that has not finished can still appear. The states the panel shows are complete and incomplete.

RAM and process limits apply per SSH user, not per SSH group

LowNot fixed yetwcenter / djangoeurope SSH hosting

Fauzi Susanto · LinkedIn

CVSS 3.1 3.3, Low

CWE-770 — CWE-770 (Allocation of Resources Without Limits or Throttling) covers a group’s users together using more than the group’s plan.

A plan sets the RAM and the number of processes of an SSH group, and a group can have several SSH users, five on X12. The server holds each user to the plan’s values on its own. RAM is the memory limit of the user’s own cgroup slice, and processes are the user’s own process limit. Nothing holds the group’s users together, so the five users of an X12 group can use 2 GB of RAM and 400 processes each.

Confirmed. Classified as Low. It takes an account with a plan for several users. The group uses more RAM and processes than its plan pays for, and on a shared server that comes from other customers. Each user is still held to the plan’s values. Not fixed yet, and there is no quick fix. On our servers systemd gives every user a cgroup slice of its own, and there is no supported parent cgroup that holds the users of a group together. Moving their processes into one would break the users’ own services. Splitting the plan between the users would leave each of five X12 users about 400 MB, which would hurt customers far more than it protects anyone. We are looking for a way to hold a group to its plan as a whole, so this may be fixed in the future. Until then each user stays within the plan’s values. Each group’s total use is measured every 10 minutes, and the group’s users get an informational mail when it comes close to a limit or goes over it. Disk space is a quota for the whole group and is not affected. A user’s own limits above the plan were a separate issue, fixed in 2.40.2.

Bank-transfer payment reference taken from the URL

LowFixedwcenter / djangoeurope payments

Anupam Giri · LinkedIn

CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:L/A:N = 3.1, Low — limited impact.

CWE-639 · CWE-472 — CWE-639 (Authorization Bypass Through User-Controlled Key / IDOR). CWE-472 (External Control of Assumed-Immutable Web Parameter) covers taking CID and reference from the URL.

The bank-transfer page showed CID and payment reference from the URL query string, with no check that those values belong to the logged-in account. A crafted link could make a logged-in customer see someone else’s reference and CID and put them on a real bank transfer.

Holds. Accepted at Low, not Medium. Bank transfers are matched by hand, so a wrong reference misdirects nothing by itself. For the attack to pay off, a customer would have to follow a crafted link and send a real transfer, and we would then have to credit it to an account whose holder does not match the sender. Even then the attacker gains panel credit, not money, and the first complaint about a missing payment exposes and reverses it. I:H is not defensible: nothing the attacker controls is written anywhere. AC:H because success depends on that manual booking — a condition outside the attacker’s control. The page now shows a reference and CID only when they come from one of the user’s own billing records; a crafted link can no longer make the page show anything the user could not already see. Our thanks for a careful report that was honest about what it could not see.

PATCH /user/ echoed the current password

LowFixedwcenter / djangoeurope API

Danu Fandiyana

CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:L/I:N/A:N = 2.2, Low

CWE-201 — CWE-201 (Insertion of Sensitive Information Into Sent Data) covers the submitted password being copied into the response.

PATCH /user/<id>/ returned the current_password just submitted in the response body. Reading the same object does not include the field, so only the write path reflected it. The credential could then sit wherever response bodies are kept: server or proxy logs, an application-performance monitor or error tracker, or a browser cache.

Confirmed. Classified as Low. AC:H and PR:H because a user had to send their password on PATCH /user/ while this was live, and reading it back means getting at those retained bodies, which normally takes operator access to the logging store. C:L is bounded to whatever was captured. I:N and A:N. Fixed. The field is write-only, so it is never echoed.

Unauthenticated GET /server/ returned every shared server

LowFixedwcenter / djangoeurope API

Raj Chaudhary

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:N = 0.0, Low under our programme. CVSS 0.0 with C:N.

CWE-862 · CWE-863 — CWE-862 (Missing Authorization). CWE-863 (Incorrect Authorization) covers is_public being treated as a cosmetic filter rather than an access check.

Signed out, GET /server/ applied the start page’s is_public selection only when the query asked for it, so omitting the parameter returned every shared server. GET /server/<id>/ answered for a server the picker does not list. ?is_public=0 returned the public set because the value was only tested for truthiness. Writes on /server/ already returned 401.

Holds as a bug. Classified as Low. The published data contains host-key fingerprints and phpMyAdmin and phpPgAdmin URLs; those data are already public. Host-key fingerprints are public by design: sshd sends them to anyone who connects, and a fingerprint does not help a man in the middle produce a matching key. phpMyAdmin and phpPgAdmin URLs are customer login pages on public DNS names, protected by database credentials. Data centres and providers can be read from IP whois. Hardware, OS and package versions are shown to customers. A server id gives no access. Signed-in customers see every shared server by design; account creation is open, so the bug saved one signup. What the bug bypassed is the start page’s selection of servers, not an access control around information. Confidentiality is none (C:N): nothing returned is restricted. We still rate it Low under our programme because the endpoint did not do what the code intended, and the is_public=0 quirk was a real defect. Fixed: signed out, the list and the detail are limited to the servers on the start page; an unlisted server’s id returns 404. Signed in, nothing changes. Both flags are read as booleans. No serializer fields were removed. The fix goes out with the next release.