Skip to content

Commit 6930385

Browse files
authored
Merge pull request #616 from Squirbie/codex/attempt-source-url-control-576
Refs #576: Reject control chars in attempt source URLs
2 parents e00dc75 + 47b3e96 commit 6930385

2 files changed

Lines changed: 40 additions & 1 deletion

File tree

app/bounty_attempts.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from sqlalchemy.orm import Session
1212

1313
from app.db import session_scope
14-
from app.ledger.service import LedgerError, validate_public_url
14+
from app.ledger.service import CONTROL_CHAR_RE, LedgerError, validate_public_url
1515
from app.models import Bounty, BountyAttempt
1616

1717
DEFAULT_ATTEMPT_TTL_SECONDS = 24 * 60 * 60
@@ -183,6 +183,10 @@ async def api_create_bounty_attempt(
183183
source = ""
184184
if not isinstance(source, str):
185185
raise HTTPException(status_code=400, detail="source_url must be a string")
186+
if CONTROL_CHAR_RE.search(source):
187+
raise HTTPException(
188+
status_code=400, detail="source_url must not contain control characters"
189+
)
186190
source = source.strip()
187191
try:
188192
source_url = validate_public_url(source) if source else None

tests/test_bounty_attempts.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,41 @@ def test_bounty_attempts_accept_empty_body_defaults_to_login(sqlite_url: str, mo
172172
assert released.json()["attempt"]["status"] == "released"
173173

174174

175+
def test_bounty_attempt_source_url_rejects_raw_control_characters(
176+
sqlite_url: str, monkeypatch
177+
) -> None:
178+
monkeypatch.setenv("MERGEWORK_COOKIE_SECRET", COOKIE_SECRET)
179+
create_schema(sqlite_url)
180+
with session_scope(sqlite_url) as session:
181+
ensure_genesis(session)
182+
bounty = create_bounty(
183+
session,
184+
repo="ramimbo/mergework",
185+
issue_number=328,
186+
issue_url="https://github.com/ramimbo/mergework/issues/328",
187+
title="Attempt source URL validation",
188+
reward_mrwk="50",
189+
max_awards=1,
190+
acceptance="Attempt source URLs should be validated before normalization.",
191+
)
192+
193+
client = TestClient(create_app(database_url=sqlite_url, webhook_secret="secret"))
194+
_set_login(client, "alice")
195+
196+
response = client.post(
197+
f"/api/v1/bounties/{bounty.id}/attempts",
198+
json={
199+
"source_url": "\thttps://github.com/ramimbo/mergework/pull/616",
200+
"ttl_seconds": 3600,
201+
},
202+
)
203+
204+
assert response.status_code == 400
205+
assert "control character" in response.json()["detail"].lower()
206+
with session_scope(sqlite_url) as session:
207+
assert session.scalars(select(BountyAttempt)).all() == []
208+
209+
175210
def test_single_award_bounty_warns_when_active_attempt_uses_available_slot(
176211
sqlite_url: str, monkeypatch
177212
) -> None:

0 commit comments

Comments
 (0)