Dismissed Approval to RCE: Unreviewed Terraform Apply in Atlantis

· Medium ·

6 min read Original article ↗

Daniel Knight

Press enter or click to view image in full size

Atlantis is an open-source tool that runs Terraform plans and applies through pull requests, letting teams review infrastructure changes and trigger execution with comments. During an assessment of its Gitea integration, Sable found that Atlantis v0.46.0 accepted a dismissed approval even after the reviewer requested changes. A developer could then apply newly submitted Terraform without a valid approval, executing commands as the Atlantis service user.

The practical effect was that anyone who could open a pull request, with no reviewer sign-off on the code actually being applied, could run arbitrary commands on the Atlantis server as its service account. That execution context also reached Atlantis’s own Gitea secrets, including a bot token that held push access to both the staging and production infrastructure repositories. This reproduction used local Terraform state and ran only benign marker commands, but in a typical deployment the same unreviewed apply would also wield whatever cloud credentials Terraform is configured with, turning a bypassed code review into direct, unaudited changes to live infrastructure. In short, a control meant to guarantee that a second person reviewed every infrastructure change could be skipped by the same developer proposing it.

The finding matches GHSA-fm5f-jg8p-x2vw, titled “Atlantis Gitea dismissed approval bypasses apply_requirements: [approved], allowing unreviewed Terraform apply.” The approval requirement remained enabled throughout this reproduction; the failure was in how Atlantis interpreted Gitea review history.

Establishing the approval requirement

The deployment used Atlantis v0.46.0 with Gitea, and the developer account could push branches and open pull requests in the staging infrastructure repository. Server-side configuration required three checks before apply:

apply_requirements: [approved, mergeable, undiverged]

The environment initially had its global apply lock engaged. The assessment enabled applies through the authenticated administrative control for testing and restored the lock afterward. The approval bypass did not bypass that global lock.

Once applies were enabled, an attempt on PR #1 without an approval failed:

Pull request must be approved according to the project's approval rules before running apply.

This established that Atlantis was evaluating the approval requirement. Sable later summarized the difference between an absent approval and an invalid historical one:

“refuses correctly when no review exists; accepts unsoundly otherwise.”

Following the review check

The relevant code was in PullIsApproved, where Atlantis retrieves Gitea reviews and returns success as soon as it encounters an APPROVED entry:

for _, review := range pullReviews {
if review.State == gitea.ReviewStateApproved {
approvalStatus.IsApproved = true
approvalStatus.ApprovedBy = review.Reviewer.UserName
approvalStatus.Date = review.Submitted

return approvalStatus, nil
}
}

Gitea retains dismissed reviews in its review history, where an entry can still have state: APPROVED alongside dismissed: true. Atlantis checked the state alone, without excluding dismissed reviews, comparing the review's commit to the current PR head, or evaluating a later request for changes before returning success.

Get Daniel Knight’s stories in your inbox

Join Medium for free to get updates from this writer.

Remember me for faster sign in

Sable’s source analysis recorded the problem:

“PullIsApproved returns IsApproved on the FIRST review with state APPROVED”

The resulting boolean reaches the approved requirement handler, which rejects the command only when IsApproved is false:

if !ctx.PullReqStatus.ApprovalStatus.IsApproved {
return fmt.Sprintf(
"Pull request must be approved according to the project's approval rules before running %s.",
cmd,
), nil
}

The requirement handler therefore accepted a false approval result from the Gitea client. Neither an empty requirements list nor a repository configuration override was needed.

From an old approval to a dismissed review

The first test established that approval was not bound to the code being applied. A reviewer approved PR head e5cf063b42, after which the developer pushed additional commits containing a local-exec provisioner. An initial apply was blocked by mergeability, prompting Sable to investigate:

“Persistent false — checking whether this is a genuine conflict via the compare API and main’s recent history.”

A local merge confirmed a real conflict. After the developer resolved it, Atlantis applied head 82348f55ae and the provisioner returned:

APPLY-RCE-PROOF-V3 uid=100(atlantis) gid=1000(atlantis) groups=1000(atlantis)

Sable then tested the stronger case: the reviewer submitted REQUEST_CHANGES, which dismissed the earlier approval. The review history now contained:

[
{
"id": 1,
"user": "reviewer",
"state": "APPROVED",
"dismissed": true,
"commit": "e5cf063b42"
},
{
"id": 2,
"user": "reviewer",
"state": "REQUEST_CHANGES",
"dismissed": false,
"commit": "edd0925082"
}
]

Despite the dismissed approval and subsequent rejection, another developer-triggered apply succeeded and returned CHANGES-REQUESTED-APPLY-EXECUTED uid=100. Sable recorded the result in its assessment notes:

“reviewer’s REQUEST_CHANGES auto-dismissed the approval”

The historical approval still satisfied Atlantis because its state remained APPROVED, even though Gitea explicitly marked it dismissed.

Reproducing command execution with a control

A separate validation pass pushed a fresh resource to PR #1 at head d1a5445f96, while the same dismissed approval and request for changes remained in the review history. The payload used a local-exec provisioner to run id and post a unique marker to the PR using the inherited Atlantis bot token. Reduced to its command execution portion, the resource was:

resource "terraform_data" "approval_bypass_mt8xlx89" {
input = "VALIDATE-DISMISSED-APPROVAL-APPLY-mt8xlx89"

provisioner "local-exec" {
command = "id"
}
}

Before triggering the target apply, the validation attempted an apply on control PR #2, which had no approval. Atlantis rejected it in comment #117 at 2026-08-25T17:27:24Z with the approval-required message.

The developer then commented atlantis apply on PR #1. One second later, comment #119 contained the marker and command output:

VALIDATE-DISMISSED-APPROVAL-APPLY-mt8xlx89 uid=100(atlantis) gid=1000(atlantis) groups=1000(atlantis)

Comment #120 recorded the successful apply at 2026-08-25T17:27:26Z:

terraform_data.approval_bypass_mt8xlx89: Creating...
terraform_data.approval_bypass_mt8xlx89: Provisioning with 'local-exec'...
terraform_data.approval_bypass_mt8xlx89: Creation complete after 1s [id=c4e620f9-56b8-d003-4e22-6de9c0d7b420]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Checks before and after execution showed the same dismissed approval and request for changes. The control demonstrated that approval enforcement was active, while the target proved that a dismissed historical approval was enough to permit newly submitted code to execute as uid=100(atlantis).

The other requirements still affected execution: mergeability rejected a genuine conflict, while the divergence check was skipped under this deployment’s branch checkout strategy. The dismissed-approval bug did not remove those requirements.

What the Atlantis account could access

The session also confirmed that code running in the Atlantis process context could access its Gitea bot token, webhook secret, and API secret. Sable investigated the token’s actual permissions rather than stopping at evidence of execution:

“Let me measure the bot token’s actual Gitea capabilities and the apply environment.”

The bot token had push permission on both staging and production infrastructure repositories, which the validation verified by creating a temporary production branch and deleting it afterward:

create_branch: status=201 name=validation-bot-write-mt8xuznl commit=08bd08baa9
delete_branch: status=204 body=""

This established production repository write access from the Atlantis execution context, although the test did not change production infrastructure. The environment used local Terraform state and terraform_data resources without cloud credentials; in other deployments, an unreviewed apply could also exercise whatever infrastructure permissions Terraform holds.

Correcting approval evaluation

The Gitea approval check must reject dismissed approvals and account for subsequent review decisions before accepting a historical APPROVED entry. Where approval of the current revision is required, it must also bind the review to the code being applied, while enforcing the repository's reviewer eligibility and approval rules.

The assessment confirmed the dismissed-approval bypass and RCE at runtime on v0.46.0. See GHSA-fm5f-jg8p-x2vw for the advisory.

Find critical zero days like this