November 1, 2026
The Mutation Everyone Tested, and the One Field Nobody Did: GraphQL Field-Level Authorization…
GraphQL’s flexibility is also where its security model tends to quietly fracture. A REST API typically ties authorization to an entire…

By T4nv1
4 min read
GraphQL's flexibility is also where its security model tends to quietly fracture. A REST API typically ties authorization to an entire endpoint — you either can or can't call it. GraphQL collapses dozens of conceptually separate operations into a handful of query and mutation entry points, which means authorization has to be enforced at a much finer grain: not just "can this user call this mutation," but "can this user set this specific field within it." That finer grain is exactly where coverage gaps hide, because it's considerably easier to review and test a mutation's primary, obvious purpose than to walk through every optional field it happens to accept.
The target, which I'll call Coppergate Learning here per their program's terms, is an online education platform where instructors manage course content and students interact with it through a shared GraphQL API.
Where the Gap Lived
Coppergate's updateEnrollment mutation was the primary way students managed their own course enrollment status — dropping a course, updating their own notification preferences for it, that kind of ordinary, self-service account management. Tested as a student account against its obvious, intended use, it behaved exactly as expected, correctly rejecting any attempt to modify another student's enrollment record.
mutation {
updateEnrollment(enrollmentId: "my-own-id", input: { notificationsEnabled: false }) {
id
notificationsEnabled
}
}mutation {
updateEnrollment(enrollmentId: "my-own-id", input: { notificationsEnabled: false }) {
id
notificationsEnabled
}
}This worked, correctly, for my own enrollment only — attempting the same mutation against another student's enrollment ID correctly failed with an authorization error, confirming object-level authorization was properly enforced for the mutation's primary, intended purpose.
Reading the Full Schema, Not Just the Fields the UI Used
The web application's own interface only ever exercised a small handful of this mutation's available input fields — the ones directly tied to the self-service features actually built into the student-facing UI. The GraphQL schema itself, retrievable through introspection, defined considerably more:
input UpdateEnrollmentInput {
notificationsEnabled: Boolean
displayName: String
finalGrade: Float
gradeLockedByInstructor: Boolean
completionStatus: String
}input UpdateEnrollmentInput {
notificationsEnabled: Boolean
displayName: String
finalGrade: Float
gradeLockedByInstructor: Boolean
completionStatus: String
}finalGrade and gradeLockedByInstructor stood out immediately — fields with no corresponding control anywhere in the student-facing interface, clearly intended for instructor-only functionality sharing the same underlying mutation rather than a separate, more carefully scoped one.
Testing Whether Object-Level Authorization Also Meant Field-Level Authorization
The mutation correctly checked whether I was authorized to modify my own enrollment record at all — that check was solid. The separate, distinct question was whether it also checked whether I, specifically as a student rather than an instructor, was authorized to set these particular fields on that record, even though it was genuinely mine.
mutation {
updateEnrollment(enrollmentId: "my-own-id", input: { finalGrade: 100.0, gradeLockedByInstructor: true }) {
id
finalGrade
}
}mutation {
updateEnrollment(enrollmentId: "my-own-id", input: { finalGrade: 100.0, gradeLockedByInstructor: true }) {
id
finalGrade
}
}This succeeded. My own test account's final grade updated to a value I'd set myself, as a student, with no instructor-role check applied to these specific fields at all — the mutation's authorization logic had correctly verified object ownership, but had never separately verified that a student account should never be permitted to set grade-related fields on any enrollment record, including their own.
Why Object-Level Correctness Isn't the Whole Picture
It's worth being explicit about why this distinction matters, because "the authorization check works" is true in a narrow, incomplete sense that understates real severity. Object-level authorization answers "does this record belong to the requesting user." Field-level authorization answers a genuinely separate question: "should this user, in their current role, ever be permitted to set this particular value, on any record, including their own." Coppergate's mutation had the first without the second, which meant any student could assign themselves an arbitrary final grade and lock it against instructor modification, entirely through the platform's own legitimate, properly-authenticated API.
Scope
I confirmed this exclusively against my own test enrollment record, setting and then immediately reverting the grade field to its original state once the finding was confirmed, and never tested the technique against any other student's actual enrollment.
The Report
Title: GraphQL updateEnrollment mutation enforces object-level but not field-level authorization, allowing students to set instructor-only fields including final grades
Root cause: The mutation correctly verifies that the requesting user owns the enrollment record being modified, but applies no separate, role-based check on which specific input fields a given role is permitted to set. Grade-related fields, intended for instructor-only use, are reachable and settable by any student modifying their own enrollment record.
Reproduction: The schema definition revealing fields with no corresponding UI exposure, and successful modification of a self-owned enrollment's grade fields as a student account, with the change immediately reverted after confirmation.
Impact: Any student can set their own final grade to an arbitrary value and lock it against instructor modification, directly undermining the academic integrity of the grading system through the platform's own legitimate API.
Fix recommendations:
- Add explicit, role-based field-level authorization to the mutation's resolver, rejecting any request attempting to set instructor-only fields unless the requesting user holds the instructor role for the relevant course
- Split grade-management functionality into a separate, distinctly authorized mutation entirely, rather than sharing a single mutation across both self-service student actions and instructor-only grading actions
- Audit the full schema for other mutations combining fields intended for different authorization levels within a single operation, since this pattern is easy to introduce incrementally as a mutation's input type grows over time
What Happened After
Coppergate split grade-related fields into a separately, properly authorized mutation and added role-based field validation as a backstop on the original mutation, closing both the immediate gap and the underlying pattern that produced it.
Why GraphQL Schemas Deserve a Full Read, Not Just UI-Driven Testing
This bug class is specifically a GraphQL problem in a way REST rarely produces as cleanly, because REST's endpoint-per-operation structure naturally limits what a single authorization check needs to cover. Any GraphQL mutation accepting a composite input type is worth a full schema read — via introspection where available, or careful review of the API's documentation otherwise — specifically looking for fields the actual client application never exercises, since those are consistently the least-tested, most likely candidates for a missing field-level check.