Skip to content

Secure the calendar iCal feed against unauthenticated disclosure#975

Open
GaryJones wants to merge 1 commit into
developfrom
vipplug-4-ical-feed-secret
Open

Secure the calendar iCal feed against unauthenticated disclosure#975
GaryJones wants to merge 1 commit into
developfrom
vipplug-4-ical-feed-secret

Conversation

@GaryJones
Copy link
Copy Markdown
Contributor

Summary

The calendar's .ics subscription feed is served from a nopriv AJAX handler, so it is reachable without logging in. Its only gate was user_key = md5( user_login . <single site-wide secret> ), compared in non-constant time, and the feed query was never scoped to the named user — it even honoured caller-supplied author/post_status filters. Because admin-ajax runs with is_admin() true, anyone holding a feed URL could read other authors' unpublished posts, and a single leaked URL (or knowledge of the shared secret) compromised every user's feed.

This reworks the feed around a per-user, revocable secret stored in user meta. The handler resolves the supplied username to a real user, requires that user can view the calendar, and validates the secret with hash_equals() — run unconditionally against a real-or-dummy value to limit username enumeration via timing. It then runs the feed as the resolved user and restricts the query to that user's own posts unless they can edit others' (mirroring the core posts list), so a leaked URL can no longer disclose other authors' work. Caller-supplied author/post_status filters are ignored on this context, and the "Regenerate calendar feed secret" action now rotates only the current user's token rather than everyone's.

This is a deliberate hard cutover: old md5(site-secret) URLs stop working, and each user re-copies their new URL from Screen Options on the Calendar (it is minted on first view). That is the point — the old, broadly-guessable URLs are invalidated.

Fixes VIPPLUG-4.

Test plan

  • composer test:integration -- --filter CalendarIcsSubscriptionAjaxTest passes (9 tests): valid token returns the user's own feed; a leaked URL does not expose another author's unpublished posts; an editor still sees the whole pipeline; invalid, legacy md5(site-secret), and unknown-user requests are rejected; per-user secrets are isolated; and caller author/post_status filters are ignored. It also asserts the nopriv hook is registered for logged-out users, guarding the structural root cause.
  • Existing calendar tests still pass (--filter 'CalendarModule|CalendarMetadataAjax' and the Calendar unit class): 29 tests green; the dashboard query path is untouched (the new scoping is gated on the subscription context).
  • PHPCS clean on the changed files.
  • Manual: enable the feed, copy your URL from Screen Options, load it logged-out and confirm you see only your own pipeline; confirm an old URL no longer works; confirm "Regenerate" only rotates your own token.

Reviewer note: the integration harness's inner WP_Query runs in non-admin mode, where scheduled (future) posts are returned but drafts are not, so the tests use future posts — a genuinely unpublished status the harness exposes — to exercise the scoping. In production (real admin-ajax) the same scoping covers drafts and pending too.

@GaryJones GaryJones requested a review from a team as a code owner June 3, 2026 19:38
@GaryJones GaryJones added this to the Next milestone Jun 3, 2026
@GaryJones GaryJones self-assigned this Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant