From b2d2c653389b42b7c1069ebd4d20e4dec3c612ee Mon Sep 17 00:00:00 2001 From: saitejasviputta Date: Sun, 28 Jun 2026 11:34:38 +0530 Subject: [PATCH 1/6] Validate git refs before constructing archive URL --- backend/app/main.py | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index d88dc1f..3556995 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -10,9 +10,11 @@ import shutil import tempfile import uuid + from datetime import datetime, timezone from pathlib import Path from typing import List +from urllib.parse import quote import aiosqlite import httpx @@ -67,6 +69,36 @@ from .scanners.semgrep import run_semgrep from .utils.fs import ensure_dir, safe_rmtree, unzip_to_dir + +GIT_REF_RE = re.compile(r"^[A-Za-z0-9._/-]+$") + + +def validate_git_ref(ref: str) -> str: + if not ref: + raise HTTPException(status_code=400, detail="Invalid Git reference") + + if len(ref) > 255: + raise HTTPException(status_code=400, detail="Invalid Git reference") + + if not GIT_REF_RE.fullmatch(ref): + raise HTTPException(status_code=400, detail="Invalid Git reference") + + forbidden = ( + "..", + "./", + "\\", + "//", + ) + + if ( + any(token in ref for token in forbidden) + or ref.startswith("/") + or ref.endswith("/") + ): + raise HTTPException(status_code=400, detail="Invalid Git reference") + + return ref + _MAX_UPLOAD_MB_RAW = os.environ.get("MAX_UPLOAD_MB") RANKER = load_ranker() @@ -316,7 +348,6 @@ def finding_key(f: Finding): line_number, ) - def github_zip_url(repo_url: str, ref: str = "main") -> str: repo_url = repo_url.strip() m = re.match( @@ -327,7 +358,8 @@ def github_zip_url(repo_url: str, ref: str = "main") -> str: status_code=400, detail="Only GitHub repo URLs are supported right now." ) owner, repo = m.group(1), m.group(2) - return f"https://github.com/{owner}/{repo}/archive/refs/heads/{ref}.zip" + encoded_ref = quote(ref, safe="/") + return f"https://github.com/{owner}/{repo}/archive/refs/heads/{encoded_ref}.zip" ALLOWED_REDIRECT_HOSTS = { @@ -660,7 +692,8 @@ async def scan_url( archive_path = job_dir / "repo.zip" repo_dir = job_dir / "repo" ensure_dir(repo_dir) - zip_url = github_zip_url(repo_url, ref=ref) + validated_ref = validate_git_ref(ref) + zip_url = github_zip_url(repo_url, ref=validated_ref) try: await download_to_path(zip_url, archive_path) From 9c97fbaef98ce06045984a0baacd312cd91ea711 Mon Sep 17 00:00:00 2001 From: saitejasviputta Date: Sun, 28 Jun 2026 16:11:26 +0530 Subject: [PATCH 2/6] Fix import ordering --- backend/app/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/app/main.py b/backend/app/main.py index 3556995..220339a 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -348,6 +348,7 @@ def finding_key(f: Finding): line_number, ) + def github_zip_url(repo_url: str, ref: str = "main") -> str: repo_url = repo_url.strip() m = re.match( From b96fb082d9f924777a96d0fde5b262f8df59bd48 Mon Sep 17 00:00:00 2001 From: saitejasviputta Date: Sun, 28 Jun 2026 22:12:05 +0530 Subject: [PATCH 3/6] Fix import ordering --- backend/app/main.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index 220339a..131b2ab 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -10,7 +10,6 @@ import shutil import tempfile import uuid - from datetime import datetime, timezone from pathlib import Path from typing import List @@ -69,7 +68,6 @@ from .scanners.semgrep import run_semgrep from .utils.fs import ensure_dir, safe_rmtree, unzip_to_dir - GIT_REF_RE = re.compile(r"^[A-Za-z0-9._/-]+$") From cf05e4f6aad2965d83ee8d1b6c0b739245b646a7 Mon Sep 17 00:00:00 2001 From: saitejasviputta Date: Mon, 29 Jun 2026 18:17:07 +0530 Subject: [PATCH 4/6] Apply Ruff formatting --- backend/app/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/app/main.py b/backend/app/main.py index 131b2ab..f262faa 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -97,6 +97,7 @@ def validate_git_ref(ref: str) -> str: return ref + _MAX_UPLOAD_MB_RAW = os.environ.get("MAX_UPLOAD_MB") RANKER = load_ranker() From d0afcbcdbb508268f05be09c271988bb350af0a5 Mon Sep 17 00:00:00 2001 From: saitejasviputta Date: Fri, 3 Jul 2026 18:25:11 +0530 Subject: [PATCH 5/6] Add tests for Git ref validation --- backend/tests/test_scan_url.py | 48 ++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/backend/tests/test_scan_url.py b/backend/tests/test_scan_url.py index ff9c3c6..15091d2 100644 --- a/backend/tests/test_scan_url.py +++ b/backend/tests/test_scan_url.py @@ -5,6 +5,54 @@ from app.main import app +import pytest + + +@pytest.mark.parametrize( + "ref", + [ + "main", + "feature/login", + "release-1.0", + ], +) +def test_scan_url_valid_refs(ref): + res = client.post( + "/scan-url", + data={ + "repo_url": "not-a-url", + "ref": ref, + "project_name": "test_project", + }, + ) + + # Validation should reach repo URL parsing, proving the ref was accepted. + assert res.status_code == 400 + assert "Only GitHub repo URLs are supported right now." in res.json()["detail"] + + +@pytest.mark.parametrize( + "ref", + [ + "../main", + "./main", + r"main\hack", + ], +) +def test_scan_url_invalid_refs(ref): + res = client.post( + "/scan-url", + data={ + "repo_url": "https://github.com/owner/repo", + "ref": ref, + "project_name": "test_project", + }, + ) + + assert res.status_code == 400 + assert res.json()["detail"] == "Invalid Git reference" + + client = TestClient(app) From 7806037ab3ad29aea65441f94a3e8401436ca924 Mon Sep 17 00:00:00 2001 From: saitejasviputta Date: Fri, 10 Jul 2026 20:18:18 +0530 Subject: [PATCH 6/6] Fix Linting issues --- backend/tests/test_scan_url.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/tests/test_scan_url.py b/backend/tests/test_scan_url.py index 15091d2..2a474b2 100644 --- a/backend/tests/test_scan_url.py +++ b/backend/tests/test_scan_url.py @@ -1,12 +1,11 @@ from unittest.mock import AsyncMock, MagicMock, patch import httpx +import pytest from fastapi.testclient import TestClient from app.main import app -import pytest - @pytest.mark.parametrize( "ref",