fix(security): close SSRF, credential-leak, and auth bypass findings in 0.8.4
Audit of 0.8.4 found the repo's own gates green, so these came from manual
review; each is covered by a new regression test.
security/fetch.ts
- safeFetch re-attached Authorization/Cookie on a same-origin redirect that
followed a cross-origin hop (a -> b -> b), handing credentials to the second
host. Compare against the origin the caller trusted, not the previous hop.
- The private-network guard resolved the host, approved it, then let fetch
resolve again, so a low-TTL record could answer public for the check and
private for the connection. Pin the connection to the validated address,
preserving Host and TLS serverName. Opt out with pinDns: false.
- 0:0:0:0:0:ffff:127.0.0.1, ::ffff:7f00:1 and fec0::1 were not treated as
private. Add uncompressed IPv4-mapped forms, site-local IPv6, 198.18/15
and 192.0.0/24.
security/url.ts
- sanitizeUrl returned "//evil.com" verbatim via the relative-path fast path,
bypassing the host checks it had just run; in an href that navigates
cross-origin. Resolve protocol-relative input instead.
dev-server/gateway.ts
- Malformed base64 in an Authorization header threw out of checkAuth on an
unauthenticated path. Fail closed.
- split(":", 2) truncated passwords at the first colon, so a password
containing ":" could never authenticate.
- The credential compare short-circuited on length mismatch, leaking length
by timing. Extracted as verifyBasicAuth so it is testable.
authz/index.ts
- Namespace wildcards only matched the first segment, so "post:comment:*"
did not grant "post:comment:delete". Match at every depth.
uploader/operations.ts
- Validate transcoder dimensions and bitrate rather than trusting the declared
type, and reject ".." path segments.
package.json
- The brace-expansion override pinned 5.0.8, which is inside the advisory
range >=4.0.0 <5.0.9. Bump to 5.0.9; bun audit is now clean.
Verified: check:production passes (typecheck, lint, 1033 tests, format,
ASVS, public-API baseline, editor checks).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -179,17 +179,29 @@ export function ffmpegVideoTranscoder(
|
||||
options: { executable?: string; spawn?: (args: string[]) => { exited: Promise<number> } } = {},
|
||||
) {
|
||||
return async (input: string, output: string, config: VideoTranscodeOptions): Promise<void> => {
|
||||
if (!/^[\w .:\\/-]+$/.test(input) || !/^[\w .:\\/-]+$/.test(output))
|
||||
throw new Error("Invalid video path");
|
||||
const validPath = (value: string) =>
|
||||
/^[\w .:\\/-]+$/.test(value) && !value.split(/[\\/]/).includes("..");
|
||||
if (!validPath(input) || !validPath(output)) throw new Error("Invalid video path");
|
||||
if (config.format !== "mp4" && config.format !== "webm")
|
||||
throw new Error("Invalid video format");
|
||||
// These reach an ffmpeg filter string, so reject anything that is not a
|
||||
// plain positive integer rather than trusting the declared type.
|
||||
const dimension = (value: number | undefined, name: string): number | undefined => {
|
||||
if (value === undefined) return undefined;
|
||||
if (!Number.isInteger(value) || value <= 0 || value > 16384)
|
||||
throw new Error(`Invalid video ${name}`);
|
||||
return value;
|
||||
};
|
||||
const width = dimension(config.width, "width");
|
||||
const height = dimension(config.height, "height");
|
||||
const bitrate = dimension(config.videoBitrateKbps, "bitrate");
|
||||
const args = [
|
||||
options.executable ?? "ffmpeg",
|
||||
"-y",
|
||||
"-i",
|
||||
input,
|
||||
...(config.width || config.height
|
||||
? ["-vf", `scale=${config.width ?? -2}:${config.height ?? -2}`]
|
||||
: []),
|
||||
...(config.videoBitrateKbps ? ["-b:v", `${config.videoBitrateKbps}k`] : []),
|
||||
...(width || height ? ["-vf", `scale=${width ?? -2}:${height ?? -2}`] : []),
|
||||
...(bitrate ? ["-b:v", `${bitrate}k`] : []),
|
||||
"-f",
|
||||
config.format,
|
||||
output,
|
||||
|
||||
Reference in New Issue
Block a user