@@ -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" : "\t https://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+
175210def test_single_award_bounty_warns_when_active_attempt_uses_available_slot (
176211 sqlite_url : str , monkeypatch
177212) -> None :
0 commit comments