Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from datetime import datetime, timezone
from pathlib import Path
from typing import List
from urllib.parse import quote

import aiosqlite
import httpx
Expand Down Expand Up @@ -67,6 +68,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()

Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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)
Expand Down
47 changes: 47 additions & 0 deletions backend/tests/test_scan_url.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,57 @@
from unittest.mock import AsyncMock, MagicMock, patch

import httpx
import pytest
from fastapi.testclient import TestClient

from app.main import app


@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)


Expand Down
Loading