Skip to content

fix(observability): fall back instead of 500 on a malformed ?timezone= - #4182

Open
ntdat812 wants to merge 1 commit into
volcengine:mainfrom
ntdat812:fix/usage-audit-malformed-timezone
Open

fix(observability): fall back instead of 500 on a malformed ?timezone=#4182
ntdat812 wants to merge 1 commit into
volcengine:mainfrom
ntdat812:fix/usage-audit-malformed-timezone

Conversation

@ntdat812

Copy link
Copy Markdown

Description

The three Console BFF endpoints (/dashboard/summary, /tokens, /context-commits) accept ?timezone= as a free-form query parameter with no pattern=. A malformed value returns HTTP 500 instead of falling back to the server default.

ZoneInfo() rejects a bad key in two different ways, and only one of them was handled:

input ZoneInfo() raises before after
Asia/Shanghai 200 200
Not/AZone ZoneInfoNotFoundError 200 (documented fallback) 200
.. ValueError 500 200
../../etc/passwd ValueError 500 200
/UTC ValueError 500 200
Asia/ ValueError 500 200
. ValueError 500 200

The malformed forms fail validation before any lookup — ZoneInfoNotFoundError is a KeyError subclass, so except ZoneInfoNotFoundError never sees them. openviking/server/app.py registers handlers for OpenVikingError, RequestValidationError and StarletteHTTPException but no generic Exception handler, so the ValueError reaches Starlette's ServerErrorMiddleware.

Status codes above are measured, driving the real router over httpx.ASGITransport(..., raise_app_exceptions=False).

This is a robustness bug, not a path-traversal one — ZoneInfo is what rejects ../../etc/passwd, and it rejects it correctly. The defect is that the rejection is not caught.

Human Involvement

  • A human participated in the implementation or review loop
  • This PR was generated entirely by AI agents without human participation in the loop

Related Issue

None — found while reading usage_audit for #4173.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)

Changes Made

  • resolve_user_timezone() now catches ValueError alongside ZoneInfoNotFoundError, matching what its docstring already promises for "an unknown or empty value".
  • resolve_usage_timezone() catches the same pair plus TypeError. That value comes from the config file, where a bare timezone: 8 parses as an int; ZoneInfo(8) raises TypeError and would abort startup instead of falling back with a warning. Kept separate from the request path, where FastAPI already guarantees str | None.
  • The shared tuple is named _INVALID_TIMEZONE with a comment on why two exception types are needed, so the pair does not drift back apart.

Testing

  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have tested this on the following platforms:
    • Linux
    • macOS
    • Windows

New tests/observability/test_usage_audit_timezone.py — 19 cases: the five malformed keys against both resolvers, the non-str config value, and one end-to-end pass per malformed key through the real UsageAuditQueryService mounted on the console router (a fake service would not exercise the resolver at all).

Reverting only openviking/observability/usage_audit/time.py:

16 failed, 3 passed

With the change: 19 passed. The 3 that pass either way are the must-not-regress guards — a valid name still resolves, an unknown name still falls back, an empty value still falls back.

Whole directory: pytest tests/observability/49 passed. ruff check and ruff format --check clean on both files.

Checklist

  • My code follows the project's coding style
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation — the docstrings are the documentation here and are updated; no user-facing doc claims the old behaviour
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

ZoneInfo() rejects a bad key two different ways: an unknown but well-formed
name raises ZoneInfoNotFoundError, while a malformed one raises ValueError
before any lookup happens. Only the first was caught, so a malformed value
escaped as an unhandled exception.

The three Console endpoints take ?timezone= as a free-form query parameter
with no pattern, and there is no generic exception handler on the app, so
the request ends as a 500:

  ?timezone=Asia/Shanghai   200
  ?timezone=Not/AZone       200  (unknown -> documented fallback)
  ?timezone=..              500
  ?timezone=/UTC            500
  ?timezone=Asia/           500

resolve_usage_timezone() additionally catches TypeError: that value comes
from the config file, where a bare 'timezone: 8' parses as an int and would
abort startup rather than fall back as its docstring promises.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant