Repository navigation
Conversation
…olicy and localize DASH/HLS inputs The media server fetched whatever URL a request named, and ffmpeg followed playlist entries and redirects on its own. Every remote read (inputs, HLS/DASH resources, thumbnails, probes, recording verification, audio, webhooks, parallel ranged downloads) now goes through fetchAllowed: only http(s), no loopback/private/link-local/metadata/CGNAT/ULA addresses unless the host is trusted or listed in MEDIA_SERVER_ALLOWED_HOSTS, and each redirect hop is checked again. HLS and DASH manifests are downloaded and rewritten to local files before ffmpeg runs with a file,pipe protocol whitelist. In DASH, MPD@Location no longer changes the media base and sibling BaseURLs resolve against their parent. The webhook secret is compared in constant time.
|
@07rjain is attempting to deploy a commit to the Cap Software Inc Team on Vercel. A member of the Team first needs to authorize it. |
| await assertUrlAllowed(current, policy, resolve); | ||
| const response = await fetcher(current, withManualRedirect(init)); |
There was a problem hiding this comment.
fetchAllowed checks DNS, then gives the original hostname to fetcher, which resolves it again. A hostname in an attacker-controlled playlist can return a public address for the check and a private address for the connection. This leaves the private-address fix bypassable.
Bind each connection to an address that passed the check, including in resolveAllowedUrl.
How this was verified: Playlist resources reach fetchAllowed, but the addresses checked by assertUrlAllowed are never passed to the network connection.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/media-server/src/lib/url-policy.ts
Line: 285-286
Comment:
**DNS changes bypass the check**
`fetchAllowed` checks DNS, then gives the original hostname to `fetcher`, which resolves it again. A hostname in an attacker-controlled playlist can return a public address for the check and a private address for the connection. This leaves the private-address fix bypassable.
Bind each connection to an address that passed the check, including in `resolveAllowedUrl`.
**How this was verified:** Playlist resources reach `fetchAllowed`, but the addresses checked by `assertUrlAllowed` are never passed to the network connection.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| const response = await fetchAllowed(webhookUrl, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| "x-media-server-secret": webhookSecret, | ||
| }, | ||
| body: JSON.stringify({ kind: "audio-levels", ...body }), | ||
| redirect: "error", | ||
| signal, |
There was a problem hiding this comment.
Redirects receive the callback secret
The audio-level callback previously refused redirects. It now follows them through fetchAllowed, which sends the same x-media-server-secret and JSON body to every target. With the default policy, a redirect to another public host passes, exposing the callback secret and recording details to that host.
Keep redirect refusal for this callback, and make fetchAllowed respect redirect: "error".
How this was verified: The callback includes the secret header, and the redirect loop reuses those headers and the body without checking whether the origin changed.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/media-server/src/lib/audio-levels.ts
Line: 78-85
Comment:
**Redirects receive the callback secret**
The audio-level callback previously refused redirects. It now follows them through `fetchAllowed`, which sends the same `x-media-server-secret` and JSON body to every target. With the default policy, a redirect to another public host passes, exposing the callback secret and recording details to that host.
Keep redirect refusal for this callback, and make `fetchAllowed` respect `redirect: "error"`.
**How this was verified:** The callback includes the secret header, and the redirect loop reuses those headers and the body without checking whether the origin changed.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| "-protocol_whitelist", | ||
| "file,pipe", |
There was a problem hiding this comment.
getAudioExtractArgs now allows only file,pipe. AES-128 HLS still needs FFmpeg's crypto protocol after the key and segments have been downloaded locally, so encrypted HLS audio extraction now fails. Include crypto here, as getAudioProbeArgs and the video download command already do.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/media-server/src/lib/media-audio.ts
Line: 123-124
Comment:
**Encrypted HLS audio fails**
`getAudioExtractArgs` now allows only `file,pipe`. AES-128 HLS still needs FFmpeg's `crypto` protocol after the key and segments have been downloaded locally, so encrypted HLS audio extraction now fails. Include `crypto` here, as `getAudioProbeArgs` and the video download command already do.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| if (resolved.includes("$")) { | ||
| throw new Error("DASH template URL cannot be fetched locally"); |
There was a problem hiding this comment.
DASH fallback rejects valid templates
The generic DASH fallback now rejects every URL containing $, but the HLS converter does not cover all valid template shapes. For example, it does not read a SegmentTemplate inherited from Period. That manifest falls back here and throws on $Number$.
Previously the generic MPD kept the template for FFmpeg to expand. Expand these templates into checked local resources before rejecting the fallback.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/media-server/src/lib/media-video.ts
Line: 458-459
Comment:
**DASH fallback rejects valid templates**
The generic DASH fallback now rejects every URL containing `$`, but the HLS converter does not cover all valid template shapes. For example, it does not read a `SegmentTemplate` inherited from `Period`. That manifest falls back here and throws on `$Number$`.
Previously the generic MPD kept the template for FFmpeg to expand. Expand these templates into checked local resources before rejecting the fallback.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| return attributes.replace( | ||
| /(initialization|media|sourceURL|xlink:href|href)="([^"]+)"/gi, |
There was a problem hiding this comment.
Valid XML references stay remote
rewriteDashAttributes only matches double-quoted attributes with no spaces around =. Valid XML such as <SegmentURL media='seg-1.m4s'/> is left unchanged, so its resource is never downloaded.
This worked before because FFmpeg could fetch the remote resource. With directory BaseURL elements removed and network protocols disabled, FFmpeg now looks for a nonexistent local file or rejects the remote URL. Parse these attributes with the same quote and whitespace support as parseXmlAttributes.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/media-server/src/lib/media-video.ts
Line: 471-472
Comment:
**Valid XML references stay remote**
`rewriteDashAttributes` only matches double-quoted attributes with no spaces around `=`. Valid XML such as `<SegmentURL media='seg-1.m4s'/>` is left unchanged, so its resource is never downloaded.
This worked before because FFmpeg could fetch the remote resource. With directory `BaseURL` elements removed and network protocols disabled, FFmpeg now looks for a nonexistent local file or rejects the remote URL. Parse these attributes with the same quote and whitespace support as `parseXmlAttributes`.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| return await downloadAllowedResource( | ||
| resolved, | ||
| dirPath, | ||
| abortSignal, | ||
| limit, | ||
| ); |
There was a problem hiding this comment.
HLS downloads duplicate whole files
Each HLS resource occurrence now downloads the entire URL into a fresh file. A byte-range playlist can name one large media file hundreds of times, so this downloads and stores hundreds of full copies instead of the requested ranges. Cache downloads by resolved URL, or download the named ranges and rewrite their offsets.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/media-server/src/lib/media-video.ts
Line: 394-399
Comment:
**HLS downloads duplicate whole files**
Each HLS resource occurrence now downloads the entire URL into a fresh file. A byte-range playlist can name one large media file hundreds of times, so this downloads and stores hundreds of full copies instead of the requested ranges. Cache downloads by resolved URL, or download the named ranges and rewrite their offsets.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| test("probe does not fetch the redirect target", async () => { | ||
| process.env.MEDIA_SERVER_ALLOW_PRIVATE_URLS = "false"; | ||
| const target = privateRedirectServer(); | ||
| try { | ||
| await expect(probeVideo(target.url)).rejects.toThrow("private address"); | ||
| expect(target.secretHits()).toBe(0); |
There was a problem hiding this comment.
Redirect tests miss the redirect
These tests start with a private loopback URL while private URLs are disabled. The policy rejects the first URL, so the redirect response is never reached. All five tests would still pass if the corresponding download path followed unchecked redirects after allowing the initial URL.
Make the first hop allowed, redirect it to a different blocked destination, and assert that the first hop was requested.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/media-server/src/__tests__/lib/redirect-follow.test.ts
Line: 51-56
Comment:
**Redirect tests miss the redirect**
These tests start with a private loopback URL while private URLs are disabled. The policy rejects the first URL, so the redirect response is never reached. All five tests would still pass if the corresponding download path followed unchecked redirects after allowing the initial URL.
Make the first hop allowed, redirect it to a different blocked destination, and assert that the first hop was requested.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Split out of #2580 (media-server URL policy and DASH handling).
Problem. The media server fetched whatever URL a request named. Redirects, HLS/DASH playlist entries and webhook URLs could point at loopback, private networks or cloud metadata (169.254.169.254), and ffmpeg followed playlist entries on its own.
What changes
url-policy.ts: onlyhttp/https. Hosts that resolve to loopback, private, link-local, CGNAT or IPv6 ULA addresses are rejected unless trusted. Trusted meansS3_INTERNAL_ENDPOINT,S3_PUBLIC_ENDPOINT,CAP_AWS_ENDPOINT,MEDIA_SERVER_WEBHOOK_URL,WEB_URL, orMEDIA_SERVER_ALLOWED_HOSTS, which becomes a strict allow-list when set.MEDIA_SERVER_ALLOW_PRIVATE_URLS=trueis for local dev and tests.fetchAllowedchecks every redirect hop. It's used for inputs, thumbnails, probes (inside the probe limit and timeout), recording verification (which decodes from a checked local file), audio, audio-level webhooks, job webhooks and the opt-in parallel ranged downloads.-protocol_whitelist file,pipe.MPD@Locationis dropped and no longer changes the media base.<BaseURL>s are alternatives, so each resolves against its parent. An alternative that is missing or blocked is dropped instead of failing the job.400 URL_NOT_ALLOWEDfrom the audio and video routes.S3_INTERNAL_ENDPOINT/S3_PUBLIC_ENDPOINT/MEDIA_SERVER_WEBHOOK_URLso bundled MinIO stays trusted.packages/local-dockersetsMEDIA_SERVER_ALLOW_PRIVATE_URLS=true.Upgrade note. Self-hosts whose storage is on a private network must set
S3_INTERNAL_ENDPOINTon the media server or list the host inMEDIA_SERVER_ALLOWED_HOSTS. Otherwise processing fails withURL_NOT_ALLOWED. The bundled compose files already do this.Tests.
url-policy.test.ts,redirect-follow.test.ts(probe, audio, thumbnail, verification and parallel download don't follow a redirect to a private address), and DASH tests for Location in another directory, sibling primary/backup BaseURLs and a missing alternative. Route tests now serve fixtures over HTTP becausefile:is rejected.Do not merge until connections use checked addresses, callbacks stop forwarding secrets across redirects, and the HLS and DASH compatibility breaks are fixed.
Findings
Fix with agent prompt
Summary
This PR adds a shared URL policy across media downloads, redirects, and callbacks.
Reviews (1) · Last reviewed commit: "fix(media-server): check every remote UR..." · Reviewed by Greptile