Summary
@validate_country is stacked above @bp.route on the economy and AI-prompt endpoints, so Flask registers the unwrapped view. The country allow-list is never consulted and any string is accepted as country_id.
Location
policyengine_api/routes/economy_routes.py:16-21
policyengine_api/routes/ai_prompt_routes.py:15-20
What goes wrong
Current code:
@validate_country
@economy_bp.route(
"/<country_id>/economy/<int:policy_id>/over/<int:baseline_policy_id>",
methods=["GET"],
)
def get_economic_impact(country_id: str, policy_id: int, baseline_policy_id: int):
...
Flask's Blueprint.route captures the function object it sees at decoration time and registers that with the URL map. Because @validate_country is applied after @bp.route (decorators run bottom-up), the blueprint has already recorded the bare function; the wrapped version returned by validate_country is only bound to the module-level name and is never invoked by the router.
Effect: /fr/economy/... or /zz/economy/... reaches the handler with an unknown country, bypassing the allow-list that protects downstream code paths. Same bug on the AI-prompt blueprint.
Suggested fix
Put @bp.route on top so it registers the wrapped view:
@economy_bp.route(
"/<country_id>/economy/<int:policy_id>/over/<int:baseline_policy_id>",
methods=["GET"],
)
@validate_country
def get_economic_impact(country_id: str, policy_id: int, baseline_policy_id: int):
...
Audit the rest of the routes module for the same decorator ordering mistake and add a test that hits an invalid country_id.
Severity
Critical.
Summary
@validate_countryis stacked above@bp.routeon the economy and AI-prompt endpoints, so Flask registers the unwrapped view. The country allow-list is never consulted and any string is accepted ascountry_id.Location
policyengine_api/routes/economy_routes.py:16-21policyengine_api/routes/ai_prompt_routes.py:15-20What goes wrong
Current code:
Flask's
Blueprint.routecaptures the function object it sees at decoration time and registers that with the URL map. Because@validate_countryis applied after@bp.route(decorators run bottom-up), the blueprint has already recorded the bare function; the wrapped version returned byvalidate_countryis only bound to the module-level name and is never invoked by the router.Effect:
/fr/economy/...or/zz/economy/...reaches the handler with an unknown country, bypassing the allow-list that protects downstream code paths. Same bug on the AI-prompt blueprint.Suggested fix
Put
@bp.routeon top so it registers the wrapped view:Audit the rest of the routes module for the same decorator ordering mistake and add a test that hits an invalid
country_id.Severity
Critical.