Zammad: Anonymous Mass Assignment Through the Public Sign-up

An attacker without an account uses the GraphQL sign-up to create an already confirmed customer account that is a member of any organization and reads its tickets.

Advisory ID: TP-2026-114
Product: Zammad (web-based helpdesk and ticketing system)
Vulnerability type: Mass assignment (CWE-915)
CVE: not requested
CVSS 4.0: 9.3 (Critical) · CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N
Affected versions: <= 7.2.0
Vendor advisory: GHSA-79wh-8g2f-xj2c
Fixed in: 7.2.1
Reported: 23 August 2026

Summary

Zammad is a web-based helpdesk in which customers are grouped into organizations and see the tickets of their organization. The public GraphQL mutation userSignup accepts arbitrary columns of the new user through the objectAttributeValues field, although only first name, last name, email and password are intended. An attacker without an account sets verified to true and skips email confirmation, including for an address they do not own. With organization_id and organization_ids the same account joins every organization of the instance in a single request. The result is a logged-in customer account that reads and replies to the tickets of all customers of those organizations; the vendor advisory bundles this with a second defect through which a customer takes over foreign tickets of their organization.

Root cause

The mutation is reachable without login through allow_public_access! (app/graphql/gql/mutations/user/signup.rb:13) and hands input.to_h unfiltered to Service::User::Signup (:20-21). The ProvidesObjectAttributeValues concern writes every entry of objectAttributeValues into that hash under the name the client chose (app/graphql/gql/types/input/concerns/provides_object_attribute_values.rb:15), without checking it against the attributes released for sign-up. create_user builds the user with User.new(user_data) (app/services/service/user/signup.rb:86) and then overwrites only role_ids and source (:88-89), so verified, organization_id and organization_ids survive. The REST twin of the same feature restricts the input to firstname, lastname, email and password (app/controllers/users_controller.rb:892-894). With verified: true the lock for unconfirmed sign-ups no longer applies (lib/auth/backend/internal.rb:24), and because organizations are shared by default, membership opens every ticket of the organization.

Proof of Concept

GET /                      -> session cookie + csrf-token
POST /graphql              X-CSRF-Token: <csrf>

mutation($input: UserSignupInput!) {
  userSignup(input: $input) { success errors { message } }
}
{"input": {"email": "[email protected]", "password": "<password>",
  "firstname": "A", "lastname": "B",
  "objectAttributeValues": [
    {"name": "verified",         "value": true},
    {"name": "organization_id",  "value": 18},
    {"name": "organization_ids", "value": [1, 4, 7]}]}}

-> {"data":{"userSignup":{"success":true,"errors":null}}}

The account can log in immediately, while the same sign-up without verified is rejected with 401, and it reads tickets of foreign customers of the injected organizations with 200. organization_ids must not contain the primary organization, otherwise the mutation rejects the request.

Impact

  • Reading every non-internal article in the tickets of all customers of every organization on the instance, without an own account and without access to the mailbox.
  • Replying in foreign tickets as a supposedly confirmed customer.
  • Permanent blocking of foreign email addresses: the real mailbox owner can no longer create an account, and inbound mail from that address is attached to the attacker's account.
  • Together with the second defect described in the vendor advisory, takeover of foreign tickets, so that the original customer loses access to their own ticket.

References

Is Something Like This in Your Software?

Our team found this vulnerability in the course of its work. Have your applications tested by the same specialists, with a penetration test from turingpoint.