Spaces:
Running
Running
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.
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|