Dismissed Approval to RCE: Unreviewed Terraform Apply in Atlantis
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, Vulnetic’s autonomous AI pentester, found that Atlantis v0.46.0 accepted a dismissed approval even after the reviewer requested changes. Sable discovered, reproduced, and validated the bypass fully autonomously, without human direction at any step. 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 applyrequirements: [approved], allowing unreviewed Terraform apply." The approval requirement remained enabled throughout this reproduction; the failure was in how Atlantis interpreted Gitea review history.
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:
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:
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.
The relevant code was in PullIsApproved, where Atlantis retrieves Gitea reviews and returns success as soon as it encounters an APPROVED entry:
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.
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:
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:
Sable then tested the stronger case: the reviewer submitted REQUESTCHANGES, which dismissed the earlier approval. The review history now contained:
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 REQUESTCHANGES 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:
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:
Comment #120 recorded the successful apply at 2026-08-25T17:27:26Z:
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.
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:
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 terraformdata resources without cloud credentials; in other deployments, an unreviewed apply could also exercise whatever infrastructure permissions Terraform holds.
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, and the flaw was source-confirmed through v0.47.1. It affects every Atlantis release before 0.49.0 and is fixed in 0.49.0. The vulnerable Gitea approval path dates back to the integration's introduction in v0.28.0 (May 2024), so the bypass was most likely present for roughly two and a half years before the patch shipped in October 2026. See GHSA-fm5f-jg8p-x2vw for the advisory.