Skip to content

fix(wolves-playlist): gate video ids and thumbnail hosts before writing artwork - #955

Open
hivecommons-hive[bot] wants to merge 1 commit into
mainfrom
sec/fix-wolves-playlist-gates
Open

hivecommons-hive[bot] wants to merge 1 commit into
mainfrom
sec/fix-wolves-playlist-gates

Conversation

@hivecommons-hive

Copy link
Copy Markdown
Contributor

Summary: The maintainer-run script that refreshes cover art for the "Wolves" soundtrack playlist now checks what it is told by the YouTube listing tool before acting on it. Video ids must look like real YouTube ids before they are used to name a file, and cover images are only downloaded from YouTube's own image servers over an encrypted connection; anything else stops the run with a clear error. This closes the gap reported in the playlist cover-art finding (#954) and brings the script in line with the back-catalogue refresh script, which already does both checks.

What I need from you: Review and approve; hold-gated, no automerge.


Security Fix

Files: scripts/update-wolves-playlist.js, src/tests/wolvesPlaylistScript.test.ts
Functions: normalizePlaylistEntries, new requireVideoId, readThumbnailUrl

  • Add VIDEO_ID_PATTERN = /^[\w-]{1,64}$/ (same shape as PLAYLIST_ID_PATTERN in update-back-catalogue.js) and enforce it in requireVideoId() before the id becomes wolves-artwork/${id}.jpg and reaches join(PUBLIC_DIR, …) in downloadArtwork().
  • readThumbnailUrl() now throws unless the thumbnail parses as a URL with protocol === 'https:' and a hostname in THUMBNAIL_HOSTS (i.ytimg.com, i9.ytimg.com, yt3.ggpht.com). The previous code fell back to returning the raw URL for any other host or scheme, which fetch() then downloaded into public/.
  • Query strings are still stripped from accepted thumbnail URLs (unchanged behaviour).

Tests: fixture that used a non-YouTube host (img.example) moved to yt3.ggpht.com; added rejection cases for traversal/dotted/over-long/empty ids and for http:, foreign-host, look-alike-host, file: and unparseable thumbnails. npx vitest run src/tests/wolvesPlaylistScript.test.ts → 6 passed; npx eslint on both files clean.

Behaviour change: a playlist entry with a non-YouTube thumbnail now aborts npm run update:wolves-playlist instead of silently importing it. That is intended — the script's only legitimate source is YouTube.

Fixes #954


Filed by sec-check agent (ACMM L4/L5 — hold-gated mode). Hold-gated: human review required.

— hive: agent=sec-check backend=copilot model=claude-fable-5.1 copilot=1.0.88

…ng artwork

normalizePlaylistEntries accepted any non-empty string as a video id and
used it directly in the public/wolves-artwork/<id>.jpg path, and
readThumbnailUrl fell back to the raw URL for any host or scheme, so
downloadArtwork would fetch arbitrary origins and write the body into
public/. Require ids to match the same /^[\w-]{1,64}$/ pattern the
back-catalogue script uses, and accept only https thumbnails on YouTube's
image hosts, failing closed otherwise.

Fixes #954

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: sec-check <280983584+hivecommons-hive[bot]@users.noreply.github.com>
@hivecommons-hive hivecommons-hive Bot added the hold Work is intentionally paused. label Oct 20, 2026
@hivecommons-hive

Copy link
Copy Markdown
Contributor Author

Important

Held for human review by the hive's ACMM level gate.

This PR was opened by the "sec-check" agent while Hive policy required a human checkpoint for that agent. Agent PRs are held at ACMM L3–L5; an outreach PR that targets a repository this hive does not manage is held at every level because it publishes project-facing communication into someone else's project.

Hive will keep the hold label until a human removes it. Operators can make a deliberate one-off release during an ACMM level change with release_level_holds=true, but level changes never release this hold automatically.

@hivecommons-hive hivecommons-hive Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Read the full script at f8f5d68 plus the callers (normalizePlaylistEntries → downloadArtwork → writeFile(join(PUBLIC_DIR, track.artwork))), and ran vitest run src/tests/wolvesPlaylistScript.test.ts on this head: 6/6 pass.

This looks correct to me and does what the description says:

  • scripts/update-wolves-playlist.js:30 — VIDEO_ID_PATTERN is byte-identical to PLAYLIST_ID_PATTERN at scripts/update-back-catalogue.js:45, and requireVideoId (:46-53) is wired in at :99, so the wolves-artwork/${id}.jpg path at :111 can no longer contain /, . or an empty segment.
  • readThumbnailUrl (:74-87) now fails closed instead of returning the raw URL: non-parsable → throws, non-https: or host not in THUMBNAIL_HOSTS → throws. The old catch {} fallthrough that let file: / arbitrary origins reach fetch() at :170 is gone.
  • The test change swapping img.example for yt3.ggpht.com is the right adjustment, not a loosening — the old fixture only passed because of the fallthrough.

One behavioural note for whoever merges (not a defect): the allowlist is i.ytimg.com, i9.ytimg.com, yt3.ggpht.com only. If yt-dlp ever returns a thumbnail on another ytimg mirror the update:wolves-playlist run will now abort rather than silently pass the URL through. That is the intended fail-closed behaviour; just expect the error message to point here if it happens.

No findings. Not approving — that is a maintainer's signature.

Confidence: 5/5 (safe)

— hive: agent=reviewer backend=copilot model=claude-fable-5.1 copilot=1.0.88

@hivecommons-hive

Copy link
Copy Markdown
Contributor Author

Hive filed review findings that do not block this PR as backlog items:

Cap: 10 backlog item(s) per PR per day.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

hold Work is intentionally paused.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[sec-check] Playlist cover-art refresh trusts YouTube ids and image URLs it should check

0 participants