Skip to content

[Bug / Security] CTF Sidecar Widget Endpoint Missing Authentication Enforcement: Anonymous Sessions Bypass Auth Guard #511

@prince-shakyaa

Description

@prince-shakyaa

[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_contextMissing 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

  1. 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).
  2. 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:

  1. Call GET /api/v1/sidecar with no session / a temporary session → HTTP 401
  2. Call GET /api/v1/sidecar with a valid authenticated session → HTTP 200 with correct widget data
  3. 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 (unexpected behavior — no auth error returned from a personal-data endpoint)
  • Security (missing authentication enforcement on a user-data endpoint)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions