Conversation
| result = await add_recipients_to_campaign(campaign_id, phones) | ||
|
|
||
| return RedirectResponse( | ||
| url=f"/linguist/campaigns/{campaign_id}/recipients?success=added_{result['recipients_added']}", |
Check warning
Code scanning / CodeQL
URL redirection from remote source Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 8 months ago
To fix the vulnerability, we should ensure that campaign_id is always strictly an integer and completely trusted before embedding it into any redirect URLs. Since FastAPI normally enforces the parameter type via the function signature (campaign_id: int), this should suffice in most cases. However, to further harden the code and handle edge cases robustly (should unexpected types slip through or should this route ever be refactored), we can explicitly validate that campaign_id is an integer before constructing the redirect URL.
The fix can be implemented by:
- Explicitly checking and casting
campaign_idtointat the top of the handler function. - If conversion fails, reject the request with a 400 error.
- Only use
campaign_idin the redirect URL if we are sure it's an integer.
Alternatively, and as additional hardening, consider using type checks directly in the redirect URL construction to ensure nothing untrusted is used; or, for even more safety, use string formatting that only allows digits (e.g., {int(campaign_id)}).
No additional imports are needed; the standard exception (ValueError) can be used, and FastAPI's HTTPException is already imported.
Only lines relating to redirecting using the untrusted campaign_id need changing—in this case, within the add_campaign_recipients function, lines 468–497.
| @@ -470,6 +470,11 @@ | ||
| campaign_id: int, | ||
| phone_numbers: str = Form(...) # Comma or newline separated | ||
| ): | ||
| # Enforce campaign_id is an integer to prevent any injection | ||
| try: | ||
| safe_campaign_id = int(campaign_id) | ||
| except (ValueError, TypeError): | ||
| raise HTTPException(status_code=400, detail="Invalid campaign ID") | ||
| """Add phone numbers to a campaign""" | ||
| user = await auth.get_current_user(request) | ||
| if not user: | ||
| @@ -486,13 +491,13 @@ | ||
| result = await add_recipients_to_campaign(campaign_id, phones) | ||
|
|
||
| return RedirectResponse( | ||
| url=f"/linguist/campaigns/{campaign_id}/recipients?success=added_{result['recipients_added']}", | ||
| url=f"/linguist/campaigns/{safe_campaign_id}/recipients?success=added_{result['recipients_added']}", | ||
| status_code=303 | ||
| ) | ||
|
|
||
| except Exception as e: | ||
| return RedirectResponse( | ||
| url=f"/linguist/campaigns/{campaign_id}/recipients?error={str(e)}", | ||
| url=f"/linguist/campaigns/{safe_campaign_id}/recipients?error={str(e)}", | ||
| status_code=303 | ||
| ) | ||
|
|
|
|
||
| except Exception as e: | ||
| return RedirectResponse( | ||
| url=f"/linguist/campaigns/{campaign_id}/recipients?error={str(e)}", |
Check warning
Code scanning / CodeQL
URL redirection from remote source Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 8 months ago
To properly address the vulnerability, we should ensure that campaign_id is safe before interpolating it into the redirect path. FastAPI parses campaign_id as int, but defense in depth says we should strictly check that it is, in fact, a positive integer.
We must also ensure that the redirect URL cannot be manipulated to perform remote redirects, including via encoding tricks.
General fix:
- Validate
campaign_idis of typeintand within the expected range (e.g., positive IDs). - Alternatively, construct routes by using only validated values.
- Additionally, ensure that no user-supplied data in query parameters (such as the error message) can inadvertently create a security risk.
Detailed fix:
- Check that
campaign_idis a positive integer (greater than zero). - In the exception handler, if the campaign id is not valid, redirect to a safe fallback (e.g., campaign listing page).
- Escape or limit the
errorquery parameter message to avoid path/query confusion (e.g., limit its length, remove special characters, or avoid reflecting details). - Only perform redirects to paths beginning with
/linguist/campaigns/<integer>/recipients.
Required changes:
- In the function at line 468 (
add_campaign_recipients), validatecampaign_idexplicitly before constructing the redirect URL. - Sanitize error message before embedding in URL.
No new imports required for basic Python validation.
| @@ -491,8 +491,15 @@ | ||
| ) | ||
|
|
||
| except Exception as e: | ||
| # Validate campaign_id is a positive integer | ||
| safe_campaign_id = campaign_id if isinstance(campaign_id, int) and campaign_id > 0 else "" | ||
| # Limit error length and remove unsafe characters | ||
| safe_error = str(e) | ||
| safe_error = safe_error[:100].replace("&", "").replace("?", "") # remove query separators | ||
| if not safe_campaign_id: | ||
| return RedirectResponse(url="/linguist/campaigns?error=invalid_campaign", status_code=303) | ||
| return RedirectResponse( | ||
| url=f"/linguist/campaigns/{campaign_id}/recipients?error={str(e)}", | ||
| url=f"/linguist/campaigns/{safe_campaign_id}/recipients?error={safe_error}", | ||
| status_code=303 | ||
| ) | ||
|
|
| await remove_recipient(campaign_id, phone_number) | ||
|
|
||
| return RedirectResponse( | ||
| url=f"/linguist/campaigns/{campaign_id}/recipients?success=removed", |
Check warning
Code scanning / CodeQL
URL redirection from remote source Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 8 months ago
To fix this issue, we need to ensure that user-provided values interpolated into URLs for redirects are either strictly validated or sanitized to avoid untrusted URL redirection. In this case, since campaign_id should always be an integer, we can explicitly cast it to int at the point of formatting, guaranteeing that only a valid integer value is used. This will prevent any possibility of path manipulation or other injection attacks, even if the function signature were changed later. This edit applies to the redirect URLs constructed within the remove_campaign_recipient route (lines 515, 521).
No imports or new methods are necessary—just cast campaign_id to an integer in the string formatting for the redirect URLs.
| @@ -512,13 +512,13 @@ | ||
| await remove_recipient(campaign_id, phone_number) | ||
|
|
||
| return RedirectResponse( | ||
| url=f"/linguist/campaigns/{campaign_id}/recipients?success=removed", | ||
| url=f"/linguist/campaigns/{int(campaign_id)}/recipients?success=removed", | ||
| status_code=303 | ||
| ) | ||
|
|
||
| except Exception as e: | ||
| return RedirectResponse( | ||
| url=f"/linguist/campaigns/{campaign_id}/recipients?error={str(e)}", | ||
| url=f"/linguist/campaigns/{int(campaign_id)}/recipients?error={str(e)}", | ||
| status_code=303 | ||
| ) | ||
|
|
|
|
||
| except Exception as e: | ||
| return RedirectResponse( | ||
| url=f"/linguist/campaigns/{campaign_id}/recipients?error={str(e)}", |
Check warning
Code scanning / CodeQL
URL redirection from remote source Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 8 months ago
To mitigate open redirect risk, we must ensure that the campaign_id path parameter cannot be used by a malicious user to craft a redirect URL that exits the application's domain or route space. Since this parameter is typed as int, we should enforce strict integer validation (which FastAPI normally does), and also ensure that the redirect URL being constructed is a relative path, not an absolute URL or protocol-relative (e.g., does not start with http://, //, or includes malicious characters such as backslash).
Implementation plan:
- Explicitly validate that the campaign_id is an integer (should be handled by FastAPI, but validate defensively).
- Before performing the redirect, construct the target URL using only sanitized integers.
- Optionally, use URL-building utilities or escape/encode any variable used in the URL (though campaign_id as int does not need encoding).
- As an additional safety step, check that the redirect URL is not suspicious (does not start with
//, does not contain backslashes, etc.), though again the risk is low with int.
Given the risk is primarily with campaign_id, make sure it's int, and never allow any malformed values through. If API usage ever changes campaign_id to be a string or expands supported data types, additional validation will be required.
Specific changes:
- Add type check: If campaign_id is not int, return an error or redirect to safe location.
- Build redirect URL in a way that only accepts proper campaign_id.
- Consider adding defensive checks, e.g.:
if not isinstance(campaign_id, int): ... - No third-party libraries required; use builtin type checking.
| @@ -508,6 +508,11 @@ | ||
| if not user: | ||
| return RedirectResponse(url="/linguist/login", status_code=303) | ||
|
|
||
| # Defensive: ensure campaign_id is strictly integer (FastAPI should enforce, but double check) | ||
| if not isinstance(campaign_id, int) or campaign_id < 0: | ||
| # Redirect to safe location if campaign_id is invalid | ||
| return RedirectResponse(url="/linguist/campaigns/?error=invalid_campaign_id", status_code=303) | ||
|
|
||
| try: | ||
| await remove_recipient(campaign_id, phone_number) | ||
|
|
||
| @@ -518,7 +523,7 @@ | ||
|
|
||
| except Exception as e: | ||
| return RedirectResponse( | ||
| url=f"/linguist/campaigns/{campaign_id}/recipients?error={str(e)}", | ||
| url=f"/linguist/campaigns/{campaign_id}/recipients?error=removed_failed", | ||
| status_code=303 | ||
| ) | ||
|
|
| return JSONResponse({ | ||
| "success": True, | ||
| "message": f"Campaign started! Sent to {result['sent']} recipients.", | ||
| "data": result | ||
| }) |
Check warning
Code scanning / CodeQL
Information exposure through an exception Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 8 months ago
To fix the problem, we should ensure that internal exception details are not exposed to API clients. The best way is to mask or generalize the error information sent to the user. Specifically:
- In
start_campaign_flow(inapp/services/campaign_starter.py), do not return the raw exception message in theresultsstructure. Instead, use a generic error string such as "An internal error occurred" or "Sending failed". - Optionally, log the detailed exception message on the server for diagnostics.
- Only generic, non-sensitive information should be included in API responses.
To implement this:
- Change line(s) 151–154 in
start_campaign_flowso that the'error'field does not includestr(e)but a generic message instead. - (Optional, if you want logging) Import the standard
loggingmodule and log the exception message and stack trace. - No changes are needed in
app/linguist/campaign_routes.py, because after the above fix, the response will not include sensitive exception details anyway.
| @@ -148,10 +148,13 @@ | ||
|
|
||
| except Exception as e: | ||
| failed_count += 1 | ||
| # Log the actual error for server-side diagnostics | ||
| import logging | ||
| logging.exception(f"Error sending message to {phone}: {e}") | ||
| results.append({ | ||
| 'phone': phone, | ||
| 'status': 'failed', | ||
| 'error': str(e) | ||
| 'error': "Failed to send message due to an internal error." | ||
| }) | ||
|
|
||
| # Update campaign status |
…nsitive information Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…rough an exception Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…rough an exception Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
No description provided.