Mushari440 commited on
Commit
e453bf5
·
verified ·
1 Parent(s): ca691b5

fix: manage/private-results 403 — CSRF origin check on read-only GETs

Browse files

_origin_ok requires an Origin header, but browsers omit it on same-origin GET requests. Applying it to GET /api/manage and GET /api/private-results therefore rejected our own page with 403, so the manage panel never loaded.

The origin check is a CSRF guard and belongs on state-changing requests only; it still applies to POST /api/model/delete and /api/model/rerun. The reads remain session-gated and owner-gated, and a cross-origin caller cannot read the response because no CORS headers are served.

Files changed (1) hide show
  1. app.py +10 -5
app.py CHANGED
@@ -227,15 +227,20 @@ def logout(request: Request):
227
  return RedirectResponse("/", status_code=302)
228
 
229
 
230
- def _require_owner(request: Request) -> str:
231
  """Every management route funnels through here. The identity comes from the
232
- signed session, so a crafted POST cannot impersonate the owner."""
 
 
 
 
 
233
  user = request.session.get("user")
234
  if not user:
235
  raise HTTPException(401, "Please sign in with Hugging Face.")
236
  if user != LEADERBOARD_OWNER:
237
  raise HTTPException(403, "Only the leaderboard maintainer can manage models.")
238
- if not _origin_ok(request):
239
  raise HTTPException(403, "Request blocked (bad origin).")
240
  if not HF_TOKEN:
241
  raise HTTPException(503, "The Space has no write token configured.")
@@ -254,7 +259,7 @@ class ModelRef(BaseModel):
254
  @app.get("/api/manage")
255
  def manage(request: Request):
256
  """Every request + whether a public/private score exists. Owner-only."""
257
- _require_owner(request)
258
  api = HfApi(token=HF_TOKEN)
259
  have = {}
260
  for repo, key in ((RESULTS_REPO, "public"), (RESULTS_PRIVATE_REPO, "private")):
@@ -340,7 +345,7 @@ def model_rerun(request: Request, body: ModelRef):
340
  def private_results(request: Request):
341
  """The private scores, served only to the owner. The browser cannot read that
342
  dataset directly, so this endpoint is the only way they reach the board."""
343
- _require_owner(request)
344
  api = HfApi(token=HF_TOKEN)
345
  out = []
346
  try:
 
227
  return RedirectResponse("/", status_code=302)
228
 
229
 
230
+ def _require_owner(request: Request, check_origin: bool = True) -> str:
231
  """Every management route funnels through here. The identity comes from the
232
+ signed session, so a crafted request cannot impersonate the owner.
233
+
234
+ check_origin is the CSRF guard and applies to STATE-CHANGING requests only.
235
+ Browsers omit the Origin header on same-origin GETs, so enforcing it on a read
236
+ endpoint rejects our own page. Reads are still session-gated, and a cross-origin
237
+ read cannot see the response anyway (no CORS headers are served)."""
238
  user = request.session.get("user")
239
  if not user:
240
  raise HTTPException(401, "Please sign in with Hugging Face.")
241
  if user != LEADERBOARD_OWNER:
242
  raise HTTPException(403, "Only the leaderboard maintainer can manage models.")
243
+ if check_origin and not _origin_ok(request):
244
  raise HTTPException(403, "Request blocked (bad origin).")
245
  if not HF_TOKEN:
246
  raise HTTPException(503, "The Space has no write token configured.")
 
259
  @app.get("/api/manage")
260
  def manage(request: Request):
261
  """Every request + whether a public/private score exists. Owner-only."""
262
+ _require_owner(request, check_origin=False) # read-only GET
263
  api = HfApi(token=HF_TOKEN)
264
  have = {}
265
  for repo, key in ((RESULTS_REPO, "public"), (RESULTS_PRIVATE_REPO, "private")):
 
345
  def private_results(request: Request):
346
  """The private scores, served only to the owner. The browser cannot read that
347
  dataset directly, so this endpoint is the only way they reach the board."""
348
+ _require_owner(request, check_origin=False) # read-only GET
349
  api = HfApi(token=HF_TOKEN)
350
  out = []
351
  try: