Skip to content

fix(backend): store token expiry as UTC instead of server local time - #972

Open
omlahore wants to merge 1 commit into
phasehq:mainfrom
omlahore:fix/naive-datetime-expiry
Open

fix(backend): store token expiry as UTC instead of server local time#972
omlahore wants to merge 1 commit into
phasehq:mainfrom
omlahore:fix/naive-datetime-expiry

Conversation

@omlahore

Copy link
Copy Markdown

Reported privately to security@phase.dev on 12 Aug and opening it here at the maintainers' request.

The bug

datetime.fromtimestamp(x / 1000) with no tz argument returns a naive datetime in the server's local time. With USE_TZ = True (settings.py:437), Django interprets that naive value as UTC on write, so on any host whose local timezone is not UTC the stored expiry is shifted by the UTC offset.

Where the offset is positive, a token outlives the expiry the user asked for.

Reproduction

A user requests an expiry of 2026-01-01T00:00:00Z, sent as epoch ms 1767225600000:

server TZ stored before drift stored after
UTC 2026-01-01T00:00:00+00:00 +0.0h 2026-01-01T00:00:00+00:00
Asia/Kolkata 2026-01-01T05:30:00+00:00 +5.5h 2026-01-01T00:00:00+00:00
America/New_York 2025-12-31T19:00:00+00:00 -5.0h 2026-01-01T00:00:00+00:00

Call sites

File What
graphene/mutations/service_accounts.py service account token expiry (x2)
graphene/mutations/environment.py token expiry (x2)
graphene/mutations/lockbox.py lockbox expiry
ee/authentication/scim/graphene/queries.py SCIM log filtering (read path, lower impact)

The codebase already does this correctly in api/views/audit.py and backend/schema.py, which is what made these stand out. This PR just makes the remaining sites match, including the timezone as dt_timezone alias those files already use (needed in environment.py, which imports Django's timezone separately).

Scope

The shipped docker-compose does not set TZ and Docker defaults to UTC, so a default self-hosted deployment and Phase Cloud are unaffected. It needs a host with a non-UTC local timezone: a bare-metal or non-Docker install, a container run with TZ set, or a developer machine. Not remotely triggerable, and the drift is bounded by the UTC offset.

This also silences RuntimeWarning: DateTimeField received a naive datetime while time zone support is active on these writes.

Left alone

api/utils/syncing/github/actions.py:226 has the same naive call, but it formats a GitHub rate-limit message for display rather than writing a DateTimeField. Happy to include it if you'd like consistency.

datetime.fromtimestamp(x / 1000) with no tz argument returns a naive
datetime in the server's local time. With USE_TZ = True, Django stamps
that naive value as UTC, so on a host whose local timezone is not UTC
the stored expiry is shifted by the UTC offset.

Where the offset is positive (Asia/Kolkata, +5:30) a token outlives the
expiry the user asked for. Where it is negative (America/New_York, -5:00)
it expires early.

Pass tz=dt_timezone.utc at each site, matching what api/views/audit.py
and backend/schema.py already do.

Affected:
  graphene/mutations/service_accounts.py  service account token expiry (x2)
  graphene/mutations/environment.py       token expiry (x2)
  graphene/mutations/lockbox.py           lockbox expiry
  ee/authentication/scim/graphene/queries.py  SCIM log filtering (read path)

The shipped docker-compose does not set TZ and Docker defaults to UTC,
so a default deployment is unaffected. It needs a non-UTC host: a
bare-metal install, a container run with TZ set, or a dev machine.

This also silences "RuntimeWarning: DateTimeField received a naive
datetime while time zone support is active" on these writes.

Reported privately to security@phase.dev first; opening publicly at the
maintainers' request.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant