Skip to content

Fix: profile share-card cache key omits fields that affect the render - #578

Open
Deez-Automations wants to merge 1 commit into
GenAI-Security-Project:mainfrom
Deez-Automations:fix/profile-share-card-cache-key-508
Open

Fix: profile share-card cache key omits fields that affect the render#578
Deez-Automations wants to merge 1 commit into
GenAI-Security-Project:mainfrom
Deez-Automations:fix/profile-share-card-cache-key-508

Conversation

@Deez-Automations

@Deez-Automations Deez-Automations commented Aug 20, 2026

Copy link
Copy Markdown

Summary

The public /share/profile/{username}/card.png endpoint caches rendered PNG cards to disk keyed on only 4 fields: username, total_points, badges_earned, challenges_completed. Two users sharing identical stats — common at 0/0/0 right after registration — collide on the identical cache file; whichever renders first is permanently served to the other. Same-user avatar/bio/featured-badge changes are also invisible to the cache: the old card stays stale indefinitely, with no way to invalidate it short of an admin manually deleting the file (there's no TTL/eviction anywhere for this cache directory).

Beyond the linked issue's own suggested fix

The issue proposes adding avatar_type/avatar_url/avatar_emoji/bio to the key. Two real gaps in that proposal, found before writing anything:

  1. The gravatar case is still collision-prone. resolve_avatar_url() (finbot/apps/ctf/routes/profile.py) resolves the actual rendered image from the user's email when avatar_type == "gravatar", not from avatar_url at all:

    if avatar_type == "gravatar" and user and user.email:
        return gravatar_url(user.email)

    Two profiles with avatar_type="gravatar", matching stats, and no bio would still produce identical cache keys under the issue's own suggested fix. Added user_email to the key.

  2. Featured-badge re-curation is invisible. PUT /featured-badges lets a user change which of their earned badges are featured, with zero change to the 4 count fields — a re-curation would leave the card permanently stale. The same gap covers "latest badge" staleness if badge composition changes at an unchanged total count (e.g. losing one badge and gaining a different one). Added earned_badge_ids (sorted before joining — badges aren't shown in a user-chosen order outside the featured section, only composition matters there) and featured_badge_ids (kept in the user's chosen order, since that IS what's rendered).

Comparison against another open fix for the same issue

Another contributor has an independent, already-open PR (#510) for the same issue. Worth comparing openly since both are unmerged: #510 implements the issue's own suggested fix directly (avatar_type/avatar_url/avatar_emoji/bio added to the key, MD5→SHA-256 — genuinely equivalent to this PR on that part). It doesn't cover the two gaps above though, so it inherits both: a gravatar user changing their linked email still collides under #510's key, and a featured-badge re-curation still serves a stale card. Not a criticism of #510's approach, both gaps only turn up if you trace resolve_avatar_url() and the /featured-badges endpoint specifically rather than working from the issue's own field list — flagging so whoever merges has the full picture.

Fix

Extracted _profile_card_cache_key() — a small, pure, directly-testable function (this file had zero test coverage before this change) — and wired it into the route using values already in scope. Also switched hashlib.md5 to hashlib.sha256, per the issue's own optional-hardening suggestion (MD5 gets flagged by security scanners even in this non-adversarial cache-busting context). Joined badge-ID lists with \x1f (unit separator) rather than ,, closing even the theoretical (not currently reachable, since badge IDs are developer-defined slugs, never user input) join-ambiguity case for free.

Known, documented limitations (not fixed here, noted rather than silently ignored)

  • No cache eviction/TTL exists anywhere for this directory. Widening the key (as this PR does) increases key cardinality per user, so disk usage grows faster after every edit — worth a follow-up issue for the cache directory itself, out of scope for a key-correctness fix.
  • Transient avatar-fetch failures get cached permanently. If _fetch_avatar_b64 fails (timeout, non-200, oversized image), the fallback-letter card is written to the cache and served for that key indefinitely, even after the fetch would have succeeded on retry. Pre-existing, not introduced by this diff.
  • No route-level integration test. The pure cache-key function is fully covered directly, but there's no test exercising the actual route to confirm profile/user fields are threaded through correctly at the call site (e.g. an accidental future swap of avatar_url/avatar_emoji wouldn't be caught). Full route testing here needs the image-rendering pipeline (Playwright via get_renderer()), which has been unreliable in this dev environment this session — didn't want to build new, possibly-flaky test infra just for this fix. Noted as a gap, not silently skipped.

Test plan

The public /share/profile/{username}/card.png endpoint cached rendered
PNG cards keyed on only 4 fields (username, total_points, badges_earned,
challenges_completed). Two users sharing identical stats -- common at
0/0/0 right after registration -- collided on the same cache file,
serving one user's card to the other. Same-user avatar/bio/featured-
badge changes were also invisible to the cache: the old card stayed
stale until stats next changed, with no way to invalidate it short of
an admin manually deleting the file.

Extended the cache key with every field that actually affects the
rendered image, not just the linked issue's own suggested set
(avatar_type/avatar_url/avatar_emoji/bio). Two gaps found and closed
beyond what the issue proposed:

- avatar_type == "gravatar" resolves its image from the user's email
  via resolve_avatar_url(), not from avatar_url at all -- the issue's
  own suggested fix would still let two gravatar-type profiles with
  matching stats and no bio collide. Added user_email to the key.
- PUT /featured-badges lets a user re-curate which earned badges are
  featured with zero change to the 4 count fields, leaving the card
  permanently stale after a re-curation. Same class of gap covers
  "latest badge" staleness if badge composition changes at an
  unchanged total count. Added earned_badge_ids (sorted -- composition,
  not order, is what's rendered) and featured_badge_ids (kept in the
  user's chosen order, since that's what's actually displayed).

Also switched hashlib.md5 to sha256 per the issue's own optional-
hardening suggestion, and joined badge-ID lists with a separator that
can't appear in a developer-defined slug ID, closing even the
theoretical (not currently reachable) join-ambiguity case for free.

Extracted the key construction into a small, pure, directly-testable
function -- this file had zero test coverage before this change.

Resolves GenAI-Security-Project#508
Copilot AI lite review requested due to automatic review settings August 20, 2026 17:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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.

2 participants