security(proxy): prevented malicious refspec injection via input validation
- Added `_require_fetch_ref` validator to enforce strict alphanumeric character sets and disallow special git characters (e.g., `:`, `--`, `..`, `*`). - Integrated validation into `git_fetch_ref_endpoint` to block malicious refspec inputs before git execution. - Added test cases in `test_proxy_server.py` to verify rejection of attempted shell and refspec injections.
This commit is contained in:
@@ -108,6 +108,36 @@ def _require_int(value: Any, field: str) -> int:
|
||||
return value
|
||||
|
||||
|
||||
_SAFE_REF_BODY_RE = re.compile(r"[A-Za-z0-9._/-]+")
|
||||
|
||||
|
||||
def _require_fetch_ref(value: Any) -> str:
|
||||
"""Validate the base-branch ref for `/gh/v1/git/fetch_ref`.
|
||||
|
||||
The orchestrator only ever fetches a branch — a bare name (`main`,
|
||||
`farm/x/y`, `alice/fix-parser`) or `refs/heads/<name>`. Reject anything
|
||||
`git_ops._branch_refspec` would otherwise pass verbatim into the fetch
|
||||
refspec: `:` (refspec injection — write arbitrary refs in the shared
|
||||
pool), a leading `-` (argv option injection), and (via the charset) `*`
|
||||
`+` `~` `^` `@` `?` `[` `\\`, whitespace, and control bytes; plus the
|
||||
git-invalid `..` / `//` / leading-or-trailing `/` / trailing `.`|`.lock`
|
||||
forms. Normal slashy branch names still pass.
|
||||
"""
|
||||
ref = _require_str(value, "ref")
|
||||
body = ref.removeprefix("refs/heads/")
|
||||
if (
|
||||
not body
|
||||
or ref.startswith("-")
|
||||
or body.startswith("/")
|
||||
or body.endswith(("/", ".", ".lock"))
|
||||
or "//" in body
|
||||
or ".." in body
|
||||
or not _SAFE_REF_BODY_RE.fullmatch(body)
|
||||
):
|
||||
raise HTTPException(400, "invalid ref")
|
||||
return ref
|
||||
|
||||
|
||||
def _optional_slot_uid(value: Any) -> int | None:
|
||||
if value is None:
|
||||
return None
|
||||
@@ -731,7 +761,7 @@ def create_proxy_app(settings: Settings) -> FastAPI:
|
||||
async def git_fetch_ref_endpoint(request: Request) -> JSONResponse:
|
||||
data = await _json_body(request)
|
||||
repo = _require_str(data.get("repo"), "repo")
|
||||
ref = _require_str(data.get("ref"), "ref")
|
||||
ref = _require_fetch_ref(data.get("ref"))
|
||||
target = _pool_dir(settings, repo)
|
||||
remote = await asyncio.to_thread(_origin_remote_auth, target, repo, _resolve_token(settings))
|
||||
# fetch_ref is intentionally best-effort; never surfaces a 5xx.
|
||||
|
||||
Reference in New Issue
Block a user