Labels / Complexity: security, authz, api, Backend · Extremely High — 500
Problem
The position lifecycle in quantara/web_app/api/position.py is inconsistent about authentication. create_position_with_transaction_data and add_extra_deposit require Depends(verify_wallet_signature), but four state-changing or position-specific endpoints do not:
close_position (GET /api/close-position) closes a position and writes a Transaction row with no wallet check.
open_position (GET /api/open-position) writes an OutboxEvent that later flips a position to OPENED.
get_repay_data (GET /api/get-repay-data) and get_withdraw_data (GET /api/get-withdraw-all-data) return the position's contract_address, position_id, and token symbol for any caller who knows a wallet_id.
All four are also GET handlers that mutate state or return per-user financial data, so they are additionally exposed to CSRF, prefetch, and browser caching. Consequence: any caller who can guess or enumerate a position UUID (returned in the unauthenticated get_user_positions response for a known wallet_id) can close or re-open another user's position and read the contract address needed to craft repay/withdraw transactions. There is no ownership check tying position_id or wallet_id back to the authenticated caller anywhere in this module.
Root cause
@router.get("/api/close-position")
@limiter.limit(WRITE_LIMIT)
async def close_position(request: Request, position_id: UUID, transaction_hash: str) -> str:
# ← no Depends(verify_wallet_signature), no ownership check against position.user
position_status = position_db_connector.close_position(str(position_id))
position_db_connector.save_transaction(position_id=position_id, status="closed", transaction_hash=transaction_hash)
return position_status
Why this is architecturally hard
- A naive fix — adding
Depends(verify_wallet_signature) — does not close the hole by itself, because that dependency only proves the caller controls some wallet; it does not prove that wallet owns the position. The correct fix must load the Position (quantara/web_app/db/models.py) and compare position.user_id to the authenticated wallet's User record.
- The mutation-via-GET problem cannot be papered over with auth alone; the endpoints must be migrated to
POST (or the state change removed from the read path), which is a breaking change for the frontend callers in quantara/frontend/src/hooks/useClosePosition.js and useWithdrawAll.js, both of which call these routes with axiosInstance.get(...).
verify_wallet_signature relies on the in-memory nonce store in quantara/web_app/api/wallet_auth.py, so tightening these endpoints also surfaces the multi-worker nonce question — a contributor must not silently assume the existing dependency is sufficient.
- The endpoints should ideally be reworked to the same
APIError envelope used elsewhere (quantara/web_app/api/errors.py) so 401/403/404 responses are machine-readable, rather than the bare HTTPException calls used today.
Proposed design
New authorization table for the affected entry points:
| Endpoint |
Method (after fix) |
Auth |
Ownership check |
/api/close-position |
POST |
verify_wallet_signature |
position.user.wallet_id == authenticated wallet_id |
/api/open-position |
POST |
verify_wallet_signature |
same as above |
/api/get-repay-data |
POST |
verify_wallet_signature |
wallet_id from auth, not query |
/api/get-withdraw-all-data |
POST |
verify_wallet_signature |
wallet_id from auth, not query |
Prefer a reusable FastAPI dependency (e.g. Depends(require_position_owner)) rather than repeating the ownership query in each handler.
Downstream impact
The frontend call sites in quantara/frontend/src/hooks/useClosePosition.js and quantara/frontend/src/hooks/useWithdrawAll.js currently send GET requests with wallet_id/position_id in query params. They must switch to POST bodies and attach the auth headers built by getAuthHeaders() in quantara/frontend/src/utils/axios.js. This is an API contract change and must land with the frontend update in the same PR or a coordinated follow-up.
Acceptance criteria
Service
Tests
Documentation
Out of scope
Do not refactor the broader auth model or the in-memory nonce store in this issue; only enforce ownership and method correctness on these four endpoints.
Getting started
Files in scope: quantara/web_app/api/position.py, quantara/web_app/db/crud/position.py, quantara/frontend/src/hooks/useClosePosition.js, quantara/frontend/src/hooks/useWithdrawAll.js. Verify with:
cd quantara && poetry run pytest web_app/tests
cd quantara/frontend && yarn test:run
Good first files to read: quantara/web_app/api/position.py, quantara/web_app/api/vault.py (for the auth pattern), quantara/web_app/api/errors.py.
Labels / Complexity: security, authz, api, Backend · Extremely High — 500
Problem
The position lifecycle in
quantara/web_app/api/position.pyis inconsistent about authentication.create_position_with_transaction_dataandadd_extra_depositrequireDepends(verify_wallet_signature), but four state-changing or position-specific endpoints do not:close_position(GET /api/close-position) closes a position and writes aTransactionrow with no wallet check.open_position(GET /api/open-position) writes anOutboxEventthat later flips a position toOPENED.get_repay_data(GET /api/get-repay-data) andget_withdraw_data(GET /api/get-withdraw-all-data) return the position'scontract_address,position_id, and token symbol for any caller who knows awallet_id.All four are also
GEThandlers that mutate state or return per-user financial data, so they are additionally exposed to CSRF, prefetch, and browser caching. Consequence: any caller who can guess or enumerate a positionUUID(returned in the unauthenticatedget_user_positionsresponse for a knownwallet_id) can close or re-open another user's position and read the contract address needed to craft repay/withdraw transactions. There is no ownership check tyingposition_idorwallet_idback to the authenticated caller anywhere in this module.Root cause
Why this is architecturally hard
Depends(verify_wallet_signature)— does not close the hole by itself, because that dependency only proves the caller controls some wallet; it does not prove that wallet owns the position. The correct fix must load thePosition(quantara/web_app/db/models.py) and compareposition.user_idto the authenticated wallet'sUserrecord.POST(or the state change removed from the read path), which is a breaking change for the frontend callers inquantara/frontend/src/hooks/useClosePosition.jsanduseWithdrawAll.js, both of which call these routes withaxiosInstance.get(...).verify_wallet_signaturerelies on the in-memory nonce store inquantara/web_app/api/wallet_auth.py, so tightening these endpoints also surfaces the multi-worker nonce question — a contributor must not silently assume the existing dependency is sufficient.APIErrorenvelope used elsewhere (quantara/web_app/api/errors.py) so 401/403/404 responses are machine-readable, rather than the bareHTTPExceptioncalls used today.Proposed design
New authorization table for the affected entry points:
/api/close-positionverify_wallet_signatureposition.user.wallet_id == authenticated wallet_id/api/open-positionverify_wallet_signature/api/get-repay-dataverify_wallet_signature/api/get-withdraw-all-dataverify_wallet_signaturePrefer a reusable FastAPI dependency (e.g.
Depends(require_position_owner)) rather than repeating the ownership query in each handler.Downstream impact
The frontend call sites in
quantara/frontend/src/hooks/useClosePosition.jsandquantara/frontend/src/hooks/useWithdrawAll.jscurrently sendGETrequests withwallet_id/position_idin query params. They must switch toPOSTbodies and attach the auth headers built bygetAuthHeaders()inquantara/frontend/src/utils/axios.js. This is an API contract change and must land with the frontend update in the same PR or a coordinated follow-up.Acceptance criteria
Service
close_positionandopen_positionreject unauthenticated requests (401) and reject a wallet that does not own the position (403).get_repay_dataandget_withdraw_datareturn repay data only to the authenticated owner of that wallet.POSTand no longer accept state changes or per-user data overGET.Tests
quantara/web_app/tests/test_positions.py(or an equivalent new file) covers: missing auth header, wrong-owner position, and the happy path for each of the four endpoints.vitestcoverage for the newPOSTcalls.Documentation
Out of scope
Do not refactor the broader auth model or the in-memory nonce store in this issue; only enforce ownership and method correctness on these four endpoints.
Getting started
Files in scope:
quantara/web_app/api/position.py,quantara/web_app/db/crud/position.py,quantara/frontend/src/hooks/useClosePosition.js,quantara/frontend/src/hooks/useWithdrawAll.js. Verify with:Good first files to read:
quantara/web_app/api/position.py,quantara/web_app/api/vault.py(for the auth pattern),quantara/web_app/api/errors.py.