[Bug / Security] CTF Sidecar Widget Endpoint Missing Authentication Enforcement: Anonymous Sessions Bypass Auth Guard
Summary
The /api/v1/sidecar endpoint (finbot/apps/ctf/routes/sidecar.py) is intended to serve
personal CTF progress data (points, badges, challenge activity) for the authenticated user's
Vendor Portal CTF widget. However, the endpoint uses the weaker get_session_context
dependency instead of get_authenticated_session_context.
This means any unauthenticated visitor with a temporary session (auto-created for every
visitor, no email required) can call this endpoint without being challenged with a 401. The
endpoint then silently processes the request using the unauthenticated session context,
passing it to database repositories that query challenge progress, badge data, and activity
using the temporary user_id.
Affected File
finbot/apps/ctf/routes/sidecar.py · Line 31
Vulnerable Code
# sidecar.py: Line 29–33
@router.get("/sidecar")
async def get_sidecar_data(
session_context: SessionContext = Depends(get_session_context), # ← wrong dependency
db: Session = Depends(get_db),
):
"""Get CTF data for the sidecar widget"""
The get_session_context dependency (defined in finbot/core/auth/middleware.py L156–161)
never raises an error - it simply returns whatever session is associated with the
request, including fully anonymous temporary sessions:
# middleware.py: L156–161
async def get_session_context(request: Request) -> SessionContext:
"""FastAPI dependency to get normal session context
- This is used for routes that don't explicitly require authentication
- May or may not be bound to an email address (temporary vs persistent)
"""
return request.state.session_context
The correct dependency for protected endpoints is get_authenticated_session_context
(L164–177), which raises HTTP 401 when the session is temporary:
# middleware.py: L164–177
async def get_authenticated_session_context(request: Request) -> SessionContext:
"""FastAPI dependency to get authenticated session context
- Requires a non-temporary session (bound to an email address aka persistent)
- Raises 401 if the session is temporary
"""
session_context = request.state.session_context
if session_context.is_temporary:
raise HTTPException(
status_code=401,
detail="Persistent session required. Please bind your email.",
)
return session_context
Root Cause
A copy-paste / oversight error: the sidecar endpoint uses get_session_context instead of
get_authenticated_session_context. Every comparable endpoint that serves personal user
data in the CTF app uses the authenticated dependency:
| Endpoint |
Auth Dependency Used |
GET /api/v1/profile |
get_authenticated_session_context ✅ |
PUT /api/v1/profile |
get_authenticated_session_context ✅ |
PUT /api/v1/profile/featured-badges |
get_authenticated_session_context ✅ |
GET /api/v1/sidecar |
get_session_context ❌ Missing auth guard |
Impact
| Scenario |
Effect |
Unauthenticated (temporary) session calls GET /api/v1/sidecar |
Endpoint does not return 401; it silently processes the request and returns zeroed widget data. No auth error is surfaced. |
| Automated scraper / bot that never binds an email |
Can continuously poll /api/v1/sidecar and cause unnecessary DB queries on UserChallengeProgressRepository, UserBadgeRepository, CTFEventRepository with no auth guard. |
| Silent data mismatch |
A temporary user receives {"points": 0, "completed": 0, ...} — as if the endpoint works — without any indication they need to authenticate first. This masks the auth requirement from API consumers and frontend error handling. |
| Inconsistency with documented intent |
The endpoint is part of the authenticated Vendor Portal. The missing 401 breaks the expected API contract for the widget. |
Steps to Reproduce
- Make a request to
GET /api/v1/sidecar without a valid authenticated session
(or using a freshly issued session cookie that has never been bound to an email address).
- Observe: the server returns HTTP 200 with widget data (all zeroed out),
instead of HTTP 401 Unauthorized.
# Example: call the endpoint with no session cookie
curl -X GET https://owasp-finbot-ctf.org/api/v1/sidecar
# Expected: HTTP 401 Unauthorized
# Actual: HTTP 200 OK → {"points": 0, "completed": 0, "total": <N>, ...}
Comparison with Correct Pattern
The GET /api/v1/profile endpoint (same app, same routes package) uses the correct
dependency:
# profile.py: L301–313 (correct pattern)
@router.get("", response_model=ProfileResponse)
async def get_own_profile(
session_context: SessionContext = Depends(get_authenticated_session_context), # ✅
db: Session = Depends(get_db),
):
Proposed Fix
Change the dependency in sidecar.py from get_session_context to
get_authenticated_session_context:
-from finbot.core.auth.middleware import get_session_context
+from finbot.core.auth.middleware import get_authenticated_session_context
@router.get("/sidecar")
async def get_sidecar_data(
- session_context: SessionContext = Depends(get_session_context),
+ session_context: SessionContext = Depends(get_authenticated_session_context),
db: Session = Depends(get_db),
):
This single-line change ensures the endpoint returns HTTP 401 Unauthorized for any caller
that has not bound an email address to their session — consistent with all other
personal-data endpoints in the CTF app.
Verification Plan
After applying the fix:
- Call
GET /api/v1/sidecar with no session / a temporary session → HTTP 401
- Call
GET /api/v1/sidecar with a valid authenticated session → HTTP 200 with correct widget data
- Existing Vendor Portal CTF widget continues to work correctly for authenticated users.
Affected Files
| File |
Lines |
Issue |
finbot/apps/ctf/routes/sidecar.py |
10, 31 |
Uses get_session_context instead of get_authenticated_session_context |
Additional Notes
- The fix is a one-line change with no impact on authenticated users.
- No new dependencies are introduced.
- A follow-up audit of
activity.py and toolkit.py may be warranted - they currently
use get_session_context and handle the unauthenticated case inline with manual checks,
which is a less consistent pattern.
Type of Issue
[Bug / Security] CTF Sidecar Widget Endpoint Missing Authentication Enforcement: Anonymous Sessions Bypass Auth Guard
Summary
The
/api/v1/sidecarendpoint (finbot/apps/ctf/routes/sidecar.py) is intended to servepersonal CTF progress data (points, badges, challenge activity) for the authenticated user's
Vendor Portal CTF widget. However, the endpoint uses the weaker
get_session_contextdependency instead of
get_authenticated_session_context.This means any unauthenticated visitor with a temporary session (auto-created for every
visitor, no email required) can call this endpoint without being challenged with a
401. Theendpoint then silently processes the request using the unauthenticated session context,
passing it to database repositories that query challenge progress, badge data, and activity
using the temporary
user_id.Affected File
finbot/apps/ctf/routes/sidecar.py· Line 31Vulnerable Code
The
get_session_contextdependency (defined infinbot/core/auth/middleware.pyL156–161)never raises an error - it simply returns whatever session is associated with the
request, including fully anonymous temporary sessions:
The correct dependency for protected endpoints is
get_authenticated_session_context(L164–177), which raises
HTTP 401when the session is temporary:Root Cause
A copy-paste / oversight error: the sidecar endpoint uses
get_session_contextinstead ofget_authenticated_session_context. Every comparable endpoint that serves personal userdata in the CTF app uses the authenticated dependency:
GET /api/v1/profileget_authenticated_session_context✅PUT /api/v1/profileget_authenticated_session_context✅PUT /api/v1/profile/featured-badgesget_authenticated_session_context✅GET /api/v1/sidecarget_session_context❌ Missing auth guardImpact
GET /api/v1/sidecar/api/v1/sidecarand cause unnecessary DB queries onUserChallengeProgressRepository,UserBadgeRepository,CTFEventRepositorywith no auth guard.{"points": 0, "completed": 0, ...}— as if the endpoint works — without any indication they need to authenticate first. This masks the auth requirement from API consumers and frontend error handling.401breaks the expected API contract for the widget.Steps to Reproduce
GET /api/v1/sidecarwithout a valid authenticated session(or using a freshly issued session cookie that has never been bound to an email address).
instead of HTTP 401 Unauthorized.
Comparison with Correct Pattern
The
GET /api/v1/profileendpoint (same app, same routes package) uses the correctdependency:
Proposed Fix
Change the dependency in
sidecar.pyfromget_session_contexttoget_authenticated_session_context:This single-line change ensures the endpoint returns
HTTP 401 Unauthorizedfor any callerthat has not bound an email address to their session — consistent with all other
personal-data endpoints in the CTF app.
Verification Plan
After applying the fix:
GET /api/v1/sidecarwith no session / a temporary session → HTTP 401GET /api/v1/sidecarwith a valid authenticated session → HTTP 200 with correct widget dataAffected Files
finbot/apps/ctf/routes/sidecar.pyget_session_contextinstead ofget_authenticated_session_contextAdditional Notes
activity.pyandtoolkit.pymay be warranted - they currentlyuse
get_session_contextand handle the unauthenticated case inline with manual checks,which is a less consistent pattern.
Type of Issue