GHSA-M98R-6667-4WQ7

Vulnerability from github – Published: 2026-05-07 01:49 – Updated: 2026-05-14 20:53
VLAI
Summary
Aegra has cross-user run injection in /threads/{thread_id}/runs (IDOR)
Details

Impact

Aegra deployments running 0.9.0 through 0.9.6 with multiple authenticated users on a shared instance are vulnerable to a cross-tenant IDOR. Any authenticated user (User A), given another user's thread_id (User B), can:

  • Execute graph runs against User B's thread via POST /threads/{thread_id}/runs, POST /threads/{thread_id}/runs/stream, or POST /threads/{thread_id}/runs/wait
  • Read User B's full checkpoint state via the resulting run's output field
  • Inject arbitrary messages into User B's conversation history (persisted in B's checkpoint)
  • Hide their activity from User B's GET /threads/{thread_id}/runs listing because the run carries A's user_id

The streaming variant is worse — the first SSE event: values frame returns the entire prior messages array immediately on connection, no graph execution needed.

Thread IDs are UUIDs but leak through frontend URLs, server logs, observability traces, and shared links. Guessing is not required.

Patches

Fixed in 0.9.7. The three affected endpoints now perform an SQL-level user_id == authenticated_user.identity check before calling _prepare_run. When the thread exists but is owned by another user, the response is 404 Thread not found (matching the read-side pattern) to avoid leaking thread existence.

Workarounds

If upgrade is not immediately possible, register an @auth.on("threads", "create_run") handler that explicitly verifies thread ownership against the authenticated identity before allowing the operation. Without a handler, no built-in authorization runs on these write paths.

Example mitigation handler:

from langgraph_sdk import Auth

auth = Auth()

@auth.on("threads", "create_run")
async def enforce_thread_owner(ctx: Auth.types.AuthContext, value: dict):
    # Look up the thread, raise 404 if not owned by ctx.user.identity.
    # Implementation depends on your data layer.
    ...

Root cause

Aegra's authorization model delegates per-resource policy to user-defined @auth.on handlers. When no handler is registered, handle_event(...) returns None and the request proceeds (default-allow). Read endpoints in api/threads.py add a defense-in-depth user_id filter at the SQL layer, but the run-creation endpoints in api/runs.py skipped that filter. Result: out-of-the-box deployments without custom auth handlers were vulnerable.

Affected endpoints

  • POST /threads/{thread_id}/runs
  • POST /threads/{thread_id}/runs/stream
  • POST /threads/{thread_id}/runs/wait

Stateless variants (POST /runs, POST /runs/wait, POST /runs/stream) are NOT affected — they generate a fresh thread_id server-side and never accept a caller-supplied one.

Credits

  • @JoJoTheBizarre — discovered and reported the vulnerability with a precise reproducer (#336)
  • @victorjmarin and @jawhardjebbi — wrote the fix and added test coverage at unit, integration, and manual-auth e2e levels (#337)

Resources

  • Issue: https://github.com/aegra/aegra/issues/336
  • Fix PR: https://github.com/aegra/aegra/pull/337
  • Release: https://github.com/aegra/aegra/releases/tag/v0.9.7
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "aegra-api"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.9.0"
            },
            {
              "fixed": "0.9.7"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-44504"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-285",
      "CWE-639"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-07T01:49:51Z",
    "nvd_published_at": "2026-05-14T16:16:24Z",
    "severity": "HIGH"
  },
  "details": "## Impact\n\nAegra deployments running 0.9.0 through 0.9.6 with multiple authenticated users on a shared instance are vulnerable to a cross-tenant IDOR. Any authenticated user (User A), given another user\u0027s `thread_id` (User B), can:\n\n- Execute graph runs against User B\u0027s thread via `POST /threads/{thread_id}/runs`, `POST /threads/{thread_id}/runs/stream`, or `POST /threads/{thread_id}/runs/wait`\n- Read User B\u0027s full checkpoint state via the resulting run\u0027s `output` field\n- Inject arbitrary messages into User B\u0027s conversation history (persisted in B\u0027s checkpoint)\n- Hide their activity from User B\u0027s `GET /threads/{thread_id}/runs` listing because the run carries A\u0027s `user_id`\n\nThe streaming variant is worse \u2014 the first SSE `event: values` frame returns the entire prior `messages` array immediately on connection, no graph execution needed.\n\nThread IDs are UUIDs but leak through frontend URLs, server logs, observability traces, and shared links. Guessing is not required.\n\n## Patches\n\nFixed in **0.9.7**. The three affected endpoints now perform an SQL-level `user_id == authenticated_user.identity` check before calling `_prepare_run`. When the thread exists but is owned by another user, the response is `404 Thread not found` (matching the read-side pattern) to avoid leaking thread existence.\n\n## Workarounds\n\nIf upgrade is not immediately possible, register an `@auth.on(\"threads\", \"create_run\")` handler that explicitly verifies thread ownership against the authenticated identity before allowing the operation. Without a handler, no built-in authorization runs on these write paths.\n\nExample mitigation handler:\n\n```python\nfrom langgraph_sdk import Auth\n\nauth = Auth()\n\n@auth.on(\"threads\", \"create_run\")\nasync def enforce_thread_owner(ctx: Auth.types.AuthContext, value: dict):\n    # Look up the thread, raise 404 if not owned by ctx.user.identity.\n    # Implementation depends on your data layer.\n    ...\n```\n\n## Root cause\n\nAegra\u0027s authorization model delegates per-resource policy to user-defined `@auth.on` handlers. When no handler is registered, `handle_event(...)` returns `None` and the request proceeds (default-allow). Read endpoints in `api/threads.py` add a defense-in-depth `user_id` filter at the SQL layer, but the run-creation endpoints in `api/runs.py` skipped that filter. Result: out-of-the-box deployments without custom auth handlers were vulnerable.\n\n## Affected endpoints\n\n- `POST /threads/{thread_id}/runs`\n- `POST /threads/{thread_id}/runs/stream`\n- `POST /threads/{thread_id}/runs/wait`\n\nStateless variants (`POST /runs`, `POST /runs/wait`, `POST /runs/stream`) are NOT affected \u2014 they generate a fresh `thread_id` server-side and never accept a caller-supplied one.\n\n## Credits\n\n- @JoJoTheBizarre \u2014 discovered and reported the vulnerability with a precise reproducer (#336)\n- @victorjmarin and @jawhardjebbi \u2014 wrote the fix and added test coverage at unit, integration, and manual-auth e2e levels (#337)\n\n## Resources\n\n- Issue: https://github.com/aegra/aegra/issues/336\n- Fix PR: https://github.com/aegra/aegra/pull/337\n- Release: https://github.com/aegra/aegra/releases/tag/v0.9.7",
  "id": "GHSA-m98r-6667-4wq7",
  "modified": "2026-05-14T20:53:21Z",
  "published": "2026-05-07T01:49:51Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/aegra/aegra/security/advisories/GHSA-m98r-6667-4wq7"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-44504"
    },
    {
      "type": "WEB",
      "url": "https://github.com/aegra/aegra/issues/336"
    },
    {
      "type": "WEB",
      "url": "https://github.com/aegra/aegra/pull/337"
    },
    {
      "type": "WEB",
      "url": "https://github.com/aegra/aegra/commit/e1b2042254fd49072ca281bc35b3f2a3bed74b31"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/aegra/aegra"
    },
    {
      "type": "WEB",
      "url": "https://github.com/aegra/aegra/releases/tag/v0.9.7"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Aegra has cross-user run injection in /threads/{thread_id}/runs (IDOR)"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…