mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 17:23:15 +00:00
* fix: enforce a replay window on Linear webhooks (AUTHZ-001) verify_linear_signature accepted any correctly-signed body with no freshness check, so a captured request could be replayed indefinitely. Parse the signed webhookTimestamp (Unix ms) and reject requests outside a 60s window, failing closed when the field is missing or malformed — mirroring the Slack verifier. * fix: stop leaking upstream auth-error bodies into user comments get_github_token_for_user folded the raw upstream response text into the error string that becomes a Slack/Linear comment (AUTH-RESP-LEAK-01). Log the full body server-side only and return a generic "GitHub auth failed (status <code>)". Also document the accepted shared-installation-token blast radius on the bot-token-only path (AUTHZ-003). * fix: bind sandbox and token caches to repo to prevent thread-id collision A PR head-branch name is attacker-controllable and get_thread_id_from_branch derives a thread_id from its first UUID with no repo binding (TID-COLLIDE-01). The in-memory sandbox cache and the per-thread GitHub-token cache were keyed on thread_id alone, and a cached sandbox was reused after only an echo-ping, so a different repo's webhook could bind to another thread's sandbox or token. Without changing the persistent thread-id scheme: - Persist the bound repo (owner/name) in thread metadata on sandbox creation and refuse to reuse a sandbox whose bound repo does not match the current event (SandboxRepoMismatchError); the in-memory proxy also carries the binding. - Bind the GitHub-token cache entries to their repo and evict on a cross-repo read so a colliding thread_id cannot be served another repo's token. - Thread repo through the reviewer and the webhook token resolvers. * fix: scope s3:ListBucket to the releases/ prefix (F-1/IAC-04) The instance role and the GitHub deploy app role granted s3:ListBucket on the whole assets bucket. Every caller (deploy.sh, the publish/rollback scripts) only ever lists under releases/, so add a StringLike s3:prefix=releases/* condition. GetBucketLocation has no s3:prefix in its request context, so it moves to its own unconditioned statement. Also document the accepted F-2 cross-env existence-oracle residual on BatchGetSecretValue. * chore: suppress test-fixture credential false positive; document AUTHZ-002 Add a machine-level suppression for the fake Datadog key in the test_team_credentials encryption-roundtrip fixture (CWE-798, not a real credential). Clarify that the within-org thread-write path is intentional by design (AUTHZ-002) — comment only, no behavior change. * fix: casefold repo-binding keys to avoid spurious cross-repo mismatch GitHub owner/name are case-insensitive. Casefold the owner/name key on both the write (binding) and read (compare) sides — repo_cache_key and the metadata bound_repo read — so Org/Repo and org/repo resolve to one repo and a legitimate same-repo run cannot raise a spurious SandboxRepoMismatchError (Gap 2). * fix: stop leaking upstream auth body in unexpected-result branch The 2xx-but-missing-token/url branch echoed the parsed upstream response body into the user-facing error. Return a generic message and log response_data server-side only, mirroring the existing HTTPStatusError fix (Gap 4). * fix: fail closed for unbound-legacy sandboxes and catch repo mismatch Gap 1: a thread with a persisted sandbox_id but no in-memory cache and no recorded bound_repo (a pre-binding legacy thread, post-deploy) previously reconnected-and-served the sandbox to the current repo, then rebound it. Now fail closed: drop the stale id and recreate a fresh sandbox bound to this repo, logging a reconnect-with-missing-binding event. A sandbox is never served to a repo unless its binding is known and matches; new threads bind on first run unchanged. Gap 3: catch SandboxRepoMismatchError at the agent and reviewer run entrypoints, log it for alarming, and surface a clean sanitized error instead of letting an opaque deep-stack exception crash-loop the worker. * chore: suppress test-fixture credential false positive in token-TTL tests Add a machine-level suppression for the fake "ghp_secret" GitHub token used by the cached-token TTL/revocation unit tests (CWE-798). Not a real credential and not a valid PAT; scoped to the unit test only.
81 lines
2.4 KiB
Python
81 lines
2.4 KiB
Python
"""AUTH-RESP-LEAK-01: upstream auth-error bodies must not reach user-facing text."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from typing import Any
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from agent.utils import auth
|
|
|
|
_SECRET_BODY = "SENSITIVE-UPSTREAM-BODY-9999"
|
|
|
|
|
|
class _Resp:
|
|
def __init__(self, status_code: int, text: str) -> None:
|
|
self.status_code = status_code
|
|
self.text = text
|
|
|
|
def raise_for_status(self) -> None:
|
|
raise httpx.HTTPStatusError(
|
|
"boom",
|
|
request=httpx.Request("POST", "http://example.test"),
|
|
response=self, # type: ignore[arg-type]
|
|
)
|
|
|
|
def json(self) -> Any:
|
|
return {}
|
|
|
|
|
|
class _Client:
|
|
def __init__(self, resp: _Resp) -> None:
|
|
self._resp = resp
|
|
|
|
async def __aenter__(self) -> _Client:
|
|
return self
|
|
|
|
async def __aexit__(self, *_a: Any) -> None:
|
|
return None
|
|
|
|
async def post(self, *_a: Any, **_k: Any) -> _Resp:
|
|
return self._resp
|
|
|
|
|
|
class _OkResp:
|
|
"""A 2xx response whose JSON body lacks both a token and an auth url."""
|
|
|
|
def __init__(self, payload: Any) -> None:
|
|
self._payload = payload
|
|
|
|
def raise_for_status(self) -> None:
|
|
return None
|
|
|
|
def json(self) -> Any:
|
|
return self._payload
|
|
|
|
|
|
def test_http_error_returns_generic_message(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(auth, "GITHUB_OAUTH_PROVIDER_ID", "provider-id")
|
|
monkeypatch.setattr(auth, "X_SERVICE_AUTH_JWT_SECRET", "jwt-secret")
|
|
monkeypatch.setattr(httpx, "AsyncClient", lambda *a, **k: _Client(_Resp(500, _SECRET_BODY)))
|
|
|
|
result = asyncio.run(auth.get_github_token_for_user("ls-user", "tenant"))
|
|
|
|
assert "error" in result
|
|
assert _SECRET_BODY not in result["error"]
|
|
assert "500" in result["error"]
|
|
|
|
|
|
def test_unexpected_result_returns_generic_message(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""The 2xx-but-missing-token/url branch must not echo the upstream body."""
|
|
monkeypatch.setattr(auth, "GITHUB_OAUTH_PROVIDER_ID", "provider-id")
|
|
monkeypatch.setattr(auth, "X_SERVICE_AUTH_JWT_SECRET", "jwt-secret")
|
|
body = {"unexpected_field": _SECRET_BODY}
|
|
monkeypatch.setattr(httpx, "AsyncClient", lambda *a, **k: _Client(_OkResp(body)))
|
|
|
|
result = asyncio.run(auth.get_github_token_for_user("ls-user", "tenant"))
|
|
|
|
assert result == {"error": "GitHub auth returned an unexpected result"}
|
|
assert _SECRET_BODY not in result["error"]
|