PRC009 — Client can write a privileged field
| Severity | medium |
| OWASP API Security Top 10 | API3:2023 Broken Object Property Level Authorization (mass assignment) |
What it catches
A request body field that grants privileges and that the client is allowed
to send: not readOnly: true (on the field, or on one of its allOf parts,
which is how OpenAPI 3.0 makes a $ref read-only), and not inside a
read-only object. Request bodies are checked through nested objects,
allOf/oneOf/anyOf and $refs.
A field grants privileges when its name's head word is one of role, admin, administrator, superuser / super user, permission, privilege, scope, tenant (or the Portuguese papel, administrador, permissão, privilégio, with or without accents), or its plural, optionally followed by an identifier word (id, uuid, name, code, key, slug, type, level, list, and plurals).
Not checked, on purpose:
- Items of arrays. A list of child records, such as chat messages with a
role, is not the record being assigned. A privileged array field itself (permissions: [...]) is still reported. - OAuth2 token requests (a body with
grant_type): asking for ascopeis what they are for. - A role or permission assigned on purpose, for the words role,
permission, scope (and papel, permissão), never admin, superuser,
tenant:
- on an operation that addresses one grant: a segment member(ship),
collaborator, invitation, role, permission (or membro, papel,
permissão) followed by a parameter, such as
PUT /orgs/{org}/memberships/{username}(a role or permission; a scope only through its enum, below). Assigning the role is what it is for; who may call it is an authorization question (PRC010, and runtime tests). A collection (POST /members) or another resource (/permissions-check) is not one, and other privileged fields in the same body (tenantId,isAdmin) are still reported; - when its enum holds only strings made of low values (viewer, view,
reader, read, readonly, guest, commenter, member, user, basic, none, no,
access, public, anonymous, visitor, and Portuguese):
guestRole: viewer | commenter. Any other value (admin,superadmin,delete:users), or a boolean, number or null, is a privilege a client could pick.
- on an operation that addresses one grant: a segment member(ship),
collaborator, invitation, role, permission (or membro, papel,
permissão) followed by a parameter, such as
| Reported | Not reported |
|---|---|
role, userRole, roleIds, roleIDs, isAdmin, isSuperUser | adminNotes (the head word is notes) |
permissions, permissões, scopes, tenantId, tenantUuid | the same fields with readOnly: true |
the same fields in responses only, or in messages[].role | |
default_role: reader | writer | admin, isAdmin: [true, false], tenantId on /memberships/{username} | guestRole: viewer | commenter, role on /memberships/{username} |
Why it matters
Mass assignment: many frameworks copy the request body straight into the
database record. If the schema lets the client send isAdmin or role,
any user can try {"isAdmin": true} on their own profile and promote
themselves, or move their record to another tenant.
How to fix it
Mark the field readOnly: true, or remove it from the request schema, and
make the server ignore it on ordinary updates. Change privileges only through
a dedicated operation that checks who is allowed to.
role:
type: string
readOnly: true
How to ignore it
If the finding is fine on purpose, write an x-procyon-ignore on the operation,
with the reason. The finding is still reported, under Ignored, and no
longer fails the run. See ignoring a finding.
/admin/users:
post:
x-procyon-ignore:
- rule: PRC009
reason: "Only admins can call this operation; enforced by the gateway"
Examples
Reported:
/users/me:
patch:
requestBody:
content:
application/json:
schema:
properties:
name:
type: string
maxLength: 100
isAdmin:
type: boolean
Not reported: isAdmin with readOnly: true, or left out of the request
schema.
Known limits
Procyon reads the spec. If the server accepts a privileged field the spec
does not document (a hidden admin field), only a runtime test can find it;
that is planned for Procyon v0.2.