@studiomeyer/mcp-video 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/FUNDING.yml +2 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +9 -0
- package/.github/dependabot.yml +46 -0
- package/.github/workflows/ci.yml +2 -2
- package/CHANGELOG.md +157 -0
- package/CODE_OF_CONDUCT.md +7 -0
- package/ECOSYSTEM.md +35 -0
- package/README.md +26 -2
- package/SECURITY.md +11 -0
- package/dist/handlers/smart-screenshot.js +10 -3
- package/dist/handlers/smart-screenshot.js.map +1 -1
- package/dist/handlers/tts.js +6 -2
- package/dist/handlers/tts.js.map +1 -1
- package/dist/handlers/video.js +17 -7
- package/dist/handlers/video.js.map +1 -1
- package/dist/lib/error-sanitizer.d.ts +26 -0
- package/dist/lib/error-sanitizer.js +58 -0
- package/dist/lib/error-sanitizer.js.map +1 -0
- package/dist/lib/error-sanitizer.test.d.ts +1 -0
- package/dist/lib/error-sanitizer.test.js +73 -0
- package/dist/lib/error-sanitizer.test.js.map +1 -0
- package/dist/lib/ffmpeg-bin.d.ts +64 -0
- package/dist/lib/ffmpeg-bin.js +96 -0
- package/dist/lib/ffmpeg-bin.js.map +1 -0
- package/dist/lib/ffmpeg-bin.test.d.ts +18 -0
- package/dist/lib/ffmpeg-bin.test.js +169 -0
- package/dist/lib/ffmpeg-bin.test.js.map +1 -0
- package/dist/lib/ffmpeg-run.d.ts +43 -0
- package/dist/lib/ffmpeg-run.js +67 -0
- package/dist/lib/ffmpeg-run.js.map +1 -0
- package/dist/lib/ffmpeg-run.test.d.ts +1 -0
- package/dist/lib/ffmpeg-run.test.js +66 -0
- package/dist/lib/ffmpeg-run.test.js.map +1 -0
- package/dist/lib/ffmpeg-safety.d.ts +37 -0
- package/dist/lib/ffmpeg-safety.js +67 -0
- package/dist/lib/ffmpeg-safety.js.map +1 -0
- package/dist/lib/ffmpeg-safety.test.d.ts +1 -0
- package/dist/lib/ffmpeg-safety.test.js +72 -0
- package/dist/lib/ffmpeg-safety.test.js.map +1 -0
- package/dist/lib/temp-dir.d.ts +24 -0
- package/dist/lib/temp-dir.js +53 -0
- package/dist/lib/temp-dir.js.map +1 -0
- package/dist/lib/temp-dir.test.d.ts +1 -0
- package/dist/lib/temp-dir.test.js +68 -0
- package/dist/lib/temp-dir.test.js.map +1 -0
- package/dist/lib/url-guard.d.ts +41 -0
- package/dist/lib/url-guard.js +134 -0
- package/dist/lib/url-guard.js.map +1 -0
- package/dist/lib/url-guard.test.d.ts +10 -0
- package/dist/lib/url-guard.test.js +231 -0
- package/dist/lib/url-guard.test.js.map +1 -0
- package/dist/server.js +9 -4
- package/dist/server.js.map +1 -1
- package/dist/tools/engine/audio-mixer.js +5 -20
- package/dist/tools/engine/audio-mixer.js.map +1 -1
- package/dist/tools/engine/audio.js +3 -19
- package/dist/tools/engine/audio.js.map +1 -1
- package/dist/tools/engine/beat-sync.js +7 -30
- package/dist/tools/engine/beat-sync.js.map +1 -1
- package/dist/tools/engine/capture.js +7 -0
- package/dist/tools/engine/capture.js.map +1 -1
- package/dist/tools/engine/chroma-key.js +2 -11
- package/dist/tools/engine/chroma-key.js.map +1 -1
- package/dist/tools/engine/concat.js +2 -11
- package/dist/tools/engine/concat.js.map +1 -1
- package/dist/tools/engine/editing.js +12 -35
- package/dist/tools/engine/editing.js.map +1 -1
- package/dist/tools/engine/encoder.js +2 -12
- package/dist/tools/engine/encoder.js.map +1 -1
- package/dist/tools/engine/lut-presets.js +2 -11
- package/dist/tools/engine/lut-presets.js.map +1 -1
- package/dist/tools/engine/narrated-video.js +30 -39
- package/dist/tools/engine/narrated-video.js.map +1 -1
- package/dist/tools/engine/smart-screenshot.js +7 -0
- package/dist/tools/engine/smart-screenshot.js.map +1 -1
- package/dist/tools/engine/social-format.js +2 -11
- package/dist/tools/engine/social-format.js.map +1 -1
- package/dist/tools/engine/template-renderer.js +2 -11
- package/dist/tools/engine/template-renderer.js.map +1 -1
- package/dist/tools/engine/text-animations.js +2 -11
- package/dist/tools/engine/text-animations.js.map +1 -1
- package/dist/tools/engine/text-overlay.js +2 -11
- package/dist/tools/engine/text-overlay.js.map +1 -1
- package/dist/tools/engine/tts.js +11 -6
- package/dist/tools/engine/tts.js.map +1 -1
- package/dist/tools/engine/voice-effects.js +3 -20
- package/dist/tools/engine/voice-effects.js.map +1 -1
- package/package.json +6 -6
- package/src/handlers/smart-screenshot.ts +8 -3
- package/src/handlers/tts.ts +6 -2
- package/src/handlers/video.ts +14 -7
- package/src/lib/error-sanitizer.test.ts +88 -0
- package/src/lib/error-sanitizer.ts +66 -0
- package/src/lib/ffmpeg-bin.test.ts +192 -0
- package/src/lib/ffmpeg-bin.ts +111 -0
- package/src/lib/ffmpeg-run.test.ts +76 -0
- package/src/lib/ffmpeg-run.ts +110 -0
- package/src/lib/ffmpeg-safety.test.ts +88 -0
- package/src/lib/ffmpeg-safety.ts +79 -0
- package/src/lib/temp-dir.test.ts +75 -0
- package/src/lib/temp-dir.ts +58 -0
- package/src/lib/url-guard.test.ts +261 -0
- package/src/lib/url-guard.ts +143 -0
- package/src/server.ts +10 -5
- package/src/tools/engine/audio-mixer.ts +8 -21
- package/src/tools/engine/audio.ts +6 -21
- package/src/tools/engine/beat-sync.ts +10 -31
- package/src/tools/engine/capture.ts +8 -0
- package/src/tools/engine/chroma-key.ts +2 -11
- package/src/tools/engine/concat.ts +2 -11
- package/src/tools/engine/editing.ts +17 -34
- package/src/tools/engine/encoder.ts +2 -12
- package/src/tools/engine/lut-presets.ts +2 -11
- package/src/tools/engine/narrated-video.ts +26 -38
- package/src/tools/engine/smart-screenshot.ts +8 -0
- package/src/tools/engine/social-format.ts +2 -11
- package/src/tools/engine/template-renderer.ts +2 -11
- package/src/tools/engine/text-animations.ts +2 -11
- package/src/tools/engine/text-overlay.ts +2 -11
- package/src/tools/engine/tts.ts +15 -6
- package/src/tools/engine/voice-effects.ts +3 -17
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ffmpeg-run.test.js","sourceRoot":"","sources":["../../src/lib/ffmpeg-run.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ffmpeg hardening — protocol whitelist + argument validation.
|
|
3
|
+
*
|
|
4
|
+
* Two distinct threats:
|
|
5
|
+
* 1. ffmpeg can follow URLs inside HLS/DASH playlists and fetch any
|
|
6
|
+
* protocol ffmpeg was built with (file://, http, rtsp, tcp, udp...).
|
|
7
|
+
* A playlist from an attacker-controlled HTTPS host can reference
|
|
8
|
+
* `http://169.254.169.254/latest/meta-data/` and exfiltrate cloud
|
|
9
|
+
* credentials. We counter by passing -protocol_whitelist on every
|
|
10
|
+
* invocation, restricting ffmpeg to the smallest set of protocols
|
|
11
|
+
* the caller actually needs.
|
|
12
|
+
* 2. Any user-controlled path string that starts with `-` is treated
|
|
13
|
+
* by ffmpeg as a flag, not a filename. A caller passing
|
|
14
|
+
* `-i /etc/passwd -frames:v 1 -f image2` as "filename" can hijack
|
|
15
|
+
* the command. We forbid leading `-` on any input/output path.
|
|
16
|
+
*/
|
|
17
|
+
export type FfmpegProtocolSet = 'local-only' | 'https-input' | 'https-and-hls';
|
|
18
|
+
/**
|
|
19
|
+
* Prepend `-protocol_whitelist <set>` to ffmpeg args.
|
|
20
|
+
*
|
|
21
|
+
* Callers should use 'local-only' unless they genuinely need network.
|
|
22
|
+
* Position matters: ffmpeg only honours -protocol_whitelist when it appears
|
|
23
|
+
* before any `-i` input that would use it, so we always prepend.
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildFfmpegArgs(userArgs: string[], protocols?: FfmpegProtocolSet): string[];
|
|
26
|
+
/**
|
|
27
|
+
* Validate a user-supplied path that will be passed to ffmpeg as -i or output.
|
|
28
|
+
* Returns the sanitized path or throws. Rejects leading `-` (flag injection),
|
|
29
|
+
* empty strings, and values containing NUL bytes.
|
|
30
|
+
*/
|
|
31
|
+
export declare function validateFfmpegPath(p: unknown, label?: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Validate every entry in an args array used as ffmpeg filename-like tokens.
|
|
34
|
+
* Pass the list of indices that are user-controlled paths; other args
|
|
35
|
+
* (built by the caller with known flags) are not touched.
|
|
36
|
+
*/
|
|
37
|
+
export declare function validateFfmpegPaths(args: string[], userControlledIndices: number[], label?: string): void;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ffmpeg hardening — protocol whitelist + argument validation.
|
|
3
|
+
*
|
|
4
|
+
* Two distinct threats:
|
|
5
|
+
* 1. ffmpeg can follow URLs inside HLS/DASH playlists and fetch any
|
|
6
|
+
* protocol ffmpeg was built with (file://, http, rtsp, tcp, udp...).
|
|
7
|
+
* A playlist from an attacker-controlled HTTPS host can reference
|
|
8
|
+
* `http://169.254.169.254/latest/meta-data/` and exfiltrate cloud
|
|
9
|
+
* credentials. We counter by passing -protocol_whitelist on every
|
|
10
|
+
* invocation, restricting ffmpeg to the smallest set of protocols
|
|
11
|
+
* the caller actually needs.
|
|
12
|
+
* 2. Any user-controlled path string that starts with `-` is treated
|
|
13
|
+
* by ffmpeg as a flag, not a filename. A caller passing
|
|
14
|
+
* `-i /etc/passwd -frames:v 1 -f image2` as "filename" can hijack
|
|
15
|
+
* the command. We forbid leading `-` on any input/output path.
|
|
16
|
+
*/
|
|
17
|
+
const PROTOCOL_SETS = {
|
|
18
|
+
// Pure file-to-file work: editing, color, concat, audio mix, chroma.
|
|
19
|
+
'local-only': 'file,pipe,crypto,cache,fd',
|
|
20
|
+
// ffmpeg can fetch the top-level https input but cannot follow HLS segment
|
|
21
|
+
// lists or reference any other protocol. Use when ONE https URL is the input.
|
|
22
|
+
'https-input': 'file,pipe,crypto,cache,fd,https,tls,tcp',
|
|
23
|
+
// HLS master+segment playback. Still refuses http (only https), file schemes
|
|
24
|
+
// and 169.254.x.x metadata (those need to be caught upstream by url-guard).
|
|
25
|
+
'https-and-hls': 'file,pipe,crypto,cache,fd,https,tls,tcp,hls,applehttp',
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Prepend `-protocol_whitelist <set>` to ffmpeg args.
|
|
29
|
+
*
|
|
30
|
+
* Callers should use 'local-only' unless they genuinely need network.
|
|
31
|
+
* Position matters: ffmpeg only honours -protocol_whitelist when it appears
|
|
32
|
+
* before any `-i` input that would use it, so we always prepend.
|
|
33
|
+
*/
|
|
34
|
+
export function buildFfmpegArgs(userArgs, protocols = 'local-only') {
|
|
35
|
+
if (!Array.isArray(userArgs)) {
|
|
36
|
+
throw new TypeError('ffmpeg args must be an array of strings');
|
|
37
|
+
}
|
|
38
|
+
return ['-protocol_whitelist', PROTOCOL_SETS[protocols], ...userArgs];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Validate a user-supplied path that will be passed to ffmpeg as -i or output.
|
|
42
|
+
* Returns the sanitized path or throws. Rejects leading `-` (flag injection),
|
|
43
|
+
* empty strings, and values containing NUL bytes.
|
|
44
|
+
*/
|
|
45
|
+
export function validateFfmpegPath(p, label = 'path') {
|
|
46
|
+
if (typeof p !== 'string' || p.length === 0) {
|
|
47
|
+
throw new TypeError(`${label} must be a non-empty string`);
|
|
48
|
+
}
|
|
49
|
+
if (p.startsWith('-')) {
|
|
50
|
+
throw new Error(`${label} must not start with "-" (looks like a flag)`);
|
|
51
|
+
}
|
|
52
|
+
if (p.includes('\0')) {
|
|
53
|
+
throw new Error(`${label} must not contain null bytes`);
|
|
54
|
+
}
|
|
55
|
+
return p;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Validate every entry in an args array used as ffmpeg filename-like tokens.
|
|
59
|
+
* Pass the list of indices that are user-controlled paths; other args
|
|
60
|
+
* (built by the caller with known flags) are not touched.
|
|
61
|
+
*/
|
|
62
|
+
export function validateFfmpegPaths(args, userControlledIndices, label = 'path') {
|
|
63
|
+
for (const i of userControlledIndices) {
|
|
64
|
+
validateFfmpegPath(args[i], `${label}[${i}]`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=ffmpeg-safety.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ffmpeg-safety.js","sourceRoot":"","sources":["../../src/lib/ffmpeg-safety.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,MAAM,aAAa,GAAsC;IACvD,qEAAqE;IACrE,YAAY,EAAE,2BAA2B;IACzC,2EAA2E;IAC3E,8EAA8E;IAC9E,aAAa,EAAE,yCAAyC;IACxD,6EAA6E;IAC7E,4EAA4E;IAC5E,eAAe,EAAE,uDAAuD;CACzE,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAC7B,QAAkB,EAClB,YAA+B,YAAY;IAE3C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,SAAS,CAAC,yCAAyC,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,CAAC,qBAAqB,EAAE,aAAa,CAAC,SAAS,CAAC,EAAE,GAAG,QAAQ,CAAC,CAAC;AACxE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,CAAU,EAAE,KAAK,GAAG,MAAM;IAC3D,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5C,MAAM,IAAI,SAAS,CAAC,GAAG,KAAK,6BAA6B,CAAC,CAAC;IAC7D,CAAC;IACD,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,8CAA8C,CAAC,CAAC;IAC1E,CAAC;IACD,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,8BAA8B,CAAC,CAAC;IAC1D,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CACjC,IAAc,EACd,qBAA+B,EAC/B,KAAK,GAAG,MAAM;IAEd,KAAK,MAAM,CAAC,IAAI,qBAAqB,EAAE,CAAC;QACtC,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC,GAAG,CAAC,CAAC;IAChD,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buildFfmpegArgs, validateFfmpegPath, validateFfmpegPaths, } from './ffmpeg-safety.js';
|
|
3
|
+
describe('ffmpeg-safety — buildFfmpegArgs', () => {
|
|
4
|
+
it('prepends -protocol_whitelist local-only by default', () => {
|
|
5
|
+
const out = buildFfmpegArgs(['-i', 'a.mp4', 'b.mp4']);
|
|
6
|
+
expect(out[0]).toBe('-protocol_whitelist');
|
|
7
|
+
expect(out[1]).toBe('file,pipe,crypto,cache,fd');
|
|
8
|
+
expect(out.slice(2)).toEqual(['-i', 'a.mp4', 'b.mp4']);
|
|
9
|
+
});
|
|
10
|
+
it('supports https-input protocol set', () => {
|
|
11
|
+
const out = buildFfmpegArgs(['-i', 'https://example.com/s.m3u8'], 'https-input');
|
|
12
|
+
expect(out[1]).toBe('file,pipe,crypto,cache,fd,https,tls,tcp');
|
|
13
|
+
});
|
|
14
|
+
it('supports https-and-hls protocol set', () => {
|
|
15
|
+
const out = buildFfmpegArgs(['-i', 'a.m3u8'], 'https-and-hls');
|
|
16
|
+
expect(out[1]).toContain('hls');
|
|
17
|
+
expect(out[1]).toContain('applehttp');
|
|
18
|
+
});
|
|
19
|
+
it('never includes http:// (plain-text) in any protocol set', () => {
|
|
20
|
+
// Explicit check: mixing http would re-open SSRF to 169.254.x.x
|
|
21
|
+
for (const set of ['local-only', 'https-input', 'https-and-hls']) {
|
|
22
|
+
const out = buildFfmpegArgs([], set);
|
|
23
|
+
const protocols = out[1].split(',');
|
|
24
|
+
expect(protocols).not.toContain('http');
|
|
25
|
+
expect(protocols).not.toContain('rtmp');
|
|
26
|
+
expect(protocols).not.toContain('rtsp');
|
|
27
|
+
expect(protocols).not.toContain('ftp');
|
|
28
|
+
expect(protocols).not.toContain('sftp');
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
it('throws if args is not an array', () => {
|
|
32
|
+
// @ts-expect-error intentional
|
|
33
|
+
expect(() => buildFfmpegArgs('not-an-array')).toThrow(/array/);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('ffmpeg-safety — validateFfmpegPath', () => {
|
|
37
|
+
it('accepts normal file paths', () => {
|
|
38
|
+
expect(validateFfmpegPath('/home/user/x.mp4')).toBe('/home/user/x.mp4');
|
|
39
|
+
expect(validateFfmpegPath('relative/file.mov')).toBe('relative/file.mov');
|
|
40
|
+
});
|
|
41
|
+
it('rejects empty strings', () => {
|
|
42
|
+
expect(() => validateFfmpegPath('')).toThrow(/non-empty/);
|
|
43
|
+
});
|
|
44
|
+
it('rejects non-string values', () => {
|
|
45
|
+
expect(() => validateFfmpegPath(null)).toThrow();
|
|
46
|
+
expect(() => validateFfmpegPath(undefined)).toThrow();
|
|
47
|
+
expect(() => validateFfmpegPath(42)).toThrow();
|
|
48
|
+
});
|
|
49
|
+
it('rejects paths starting with "-" (flag injection)', () => {
|
|
50
|
+
expect(() => validateFfmpegPath('-i')).toThrow(/flag/);
|
|
51
|
+
expect(() => validateFfmpegPath('-protocol_whitelist')).toThrow(/flag/);
|
|
52
|
+
expect(() => validateFfmpegPath('-help')).toThrow(/flag/);
|
|
53
|
+
});
|
|
54
|
+
it('rejects paths containing NUL bytes', () => {
|
|
55
|
+
expect(() => validateFfmpegPath('safe\0-i /etc/passwd')).toThrow(/null byte/);
|
|
56
|
+
});
|
|
57
|
+
it('includes the label in error messages', () => {
|
|
58
|
+
expect(() => validateFfmpegPath('-bad', 'input')).toThrow(/input/);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('ffmpeg-safety — validateFfmpegPaths', () => {
|
|
62
|
+
it('validates only the indices passed', () => {
|
|
63
|
+
const args = ['-y', '-i', 'valid.mp4', '-c:v', 'libx264', '-foo'];
|
|
64
|
+
// Only index 2 is a user-controlled path; -foo at index 5 is a built-in flag
|
|
65
|
+
validateFfmpegPaths(args, [2]);
|
|
66
|
+
expect(() => validateFfmpegPaths(args, [5])).toThrow(/flag/);
|
|
67
|
+
});
|
|
68
|
+
it('throws when any validated arg is empty', () => {
|
|
69
|
+
expect(() => validateFfmpegPaths(['', 'x'], [0])).toThrow(/non-empty/);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
//# sourceMappingURL=ffmpeg-safety.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ffmpeg-safety.test.js","sourceRoot":"","sources":["../../src/lib/ffmpeg-safety.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAE5B,QAAQ,CAAC,iCAAiC,EAAE,GAAG,EAAE;IAC/C,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,GAAG,GAAG,eAAe,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;QACtD,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;QAC3C,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;QACjD,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,MAAM,GAAG,GAAG,eAAe,CAAC,CAAC,IAAI,EAAE,4BAA4B,CAAC,EAAE,aAAa,CAAC,CAAC;QACjF,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,yCAAyC,CAAC,CAAC;IACjE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,GAAG,GAAG,eAAe,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,eAAe,CAAC,CAAC;QAC/D,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAChC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,gEAAgE;QAChE,KAAK,MAAM,GAAG,IAAI,CAAC,YAAY,EAAE,aAAa,EAAE,eAAe,CAAU,EAAE,CAAC;YAC1E,MAAM,GAAG,GAAG,eAAe,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;YACrC,MAAM,SAAS,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACpC,MAAM,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;YACxC,MAAM,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;YACxC,MAAM,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;YACxC,MAAM,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;YACvC,MAAM,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gCAAgC,EAAE,GAAG,EAAE;QACxC,+BAA+B;QAC/B,MAAM,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,cAAc,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACjE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,oCAAoC,EAAE,GAAG,EAAE;IAClD,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACnC,MAAM,CAAC,kBAAkB,CAAC,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;QACxE,MAAM,CAAC,kBAAkB,CAAC,mBAAmB,CAAC,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;IAC5E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uBAAuB,EAAE,GAAG,EAAE;QAC/B,MAAM,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACnC,MAAM,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;QACjD,MAAM,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;QACtD,MAAM,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACvD,MAAM,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,qBAAqB,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACxE,MAAM,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,sBAAsB,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAChF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,qCAAqC,EAAE,GAAG,EAAE;IACnD,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,MAAM,IAAI,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;QAClE,6EAA6E;QAC7E,mBAAmB,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/B,MAAM,CAAC,GAAG,EAAE,CAAC,mBAAmB,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,CAAC,GAAG,EAAE,CAAC,mBAAmB,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IACzE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe temp directory helper.
|
|
3
|
+
*
|
|
4
|
+
* Motivation: many engines create predictable paths like
|
|
5
|
+
* `/tmp/narrated-video-${Date.now()}` which (a) race when two invocations
|
|
6
|
+
* hit the same millisecond, (b) leak state when the process crashes before
|
|
7
|
+
* the manual cleanup runs, and (c) are trivially overwritable by a local
|
|
8
|
+
* attacker who can guess the pattern.
|
|
9
|
+
*
|
|
10
|
+
* `withTempDir` uses `fs.mkdtemp` (unique suffix from the kernel) and
|
|
11
|
+
* always cleans up via try/finally.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Create a unique temp directory, pass it to `fn`, then clean up.
|
|
15
|
+
* Cleanup runs even if `fn` throws. Cleanup errors are logged but never
|
|
16
|
+
* re-thrown — the original error from `fn` always wins.
|
|
17
|
+
*/
|
|
18
|
+
export declare function withTempDir<T>(prefix: string, fn: (dir: string) => Promise<T>): Promise<T>;
|
|
19
|
+
/**
|
|
20
|
+
* Low-level: just create a unique temp directory, return the path.
|
|
21
|
+
* The caller is responsible for cleanup — prefer `withTempDir` when
|
|
22
|
+
* the lifetime is scoped to one function.
|
|
23
|
+
*/
|
|
24
|
+
export declare function makeTempDir(prefix: string): Promise<string>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe temp directory helper.
|
|
3
|
+
*
|
|
4
|
+
* Motivation: many engines create predictable paths like
|
|
5
|
+
* `/tmp/narrated-video-${Date.now()}` which (a) race when two invocations
|
|
6
|
+
* hit the same millisecond, (b) leak state when the process crashes before
|
|
7
|
+
* the manual cleanup runs, and (c) are trivially overwritable by a local
|
|
8
|
+
* attacker who can guess the pattern.
|
|
9
|
+
*
|
|
10
|
+
* `withTempDir` uses `fs.mkdtemp` (unique suffix from the kernel) and
|
|
11
|
+
* always cleans up via try/finally.
|
|
12
|
+
*/
|
|
13
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
import { logger } from './logger.js';
|
|
17
|
+
/**
|
|
18
|
+
* Create a unique temp directory, pass it to `fn`, then clean up.
|
|
19
|
+
* Cleanup runs even if `fn` throws. Cleanup errors are logged but never
|
|
20
|
+
* re-thrown — the original error from `fn` always wins.
|
|
21
|
+
*/
|
|
22
|
+
export async function withTempDir(prefix, fn) {
|
|
23
|
+
const base = join(tmpdir(), sanitizePrefix(prefix));
|
|
24
|
+
const dir = await mkdtemp(base);
|
|
25
|
+
try {
|
|
26
|
+
return await fn(dir);
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
await rm(dir, { recursive: true, force: true }).catch((err) => {
|
|
30
|
+
logger.warn(`temp-dir cleanup failed for ${dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Low-level: just create a unique temp directory, return the path.
|
|
36
|
+
* The caller is responsible for cleanup — prefer `withTempDir` when
|
|
37
|
+
* the lifetime is scoped to one function.
|
|
38
|
+
*/
|
|
39
|
+
export async function makeTempDir(prefix) {
|
|
40
|
+
const base = join(tmpdir(), sanitizePrefix(prefix));
|
|
41
|
+
return mkdtemp(base);
|
|
42
|
+
}
|
|
43
|
+
function sanitizePrefix(prefix) {
|
|
44
|
+
// Kill `..` sequences first so a caller can't build a traversal-looking
|
|
45
|
+
// literal segment (the join() to tmpdir() already makes traversal
|
|
46
|
+
// impossible, but we still prefer tidy paths for ops + audit logs).
|
|
47
|
+
const noTraversal = prefix.replace(/\.{2,}/g, '-');
|
|
48
|
+
const safe = noTraversal.replace(/[^a-zA-Z0-9._-]/g, '-').slice(0, 32);
|
|
49
|
+
// mkdtemp appends 6 random chars; ensure we end with a `-` so the
|
|
50
|
+
// generated suffix stays visually separated.
|
|
51
|
+
return safe.endsWith('-') ? safe : `${safe}-`;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=temp-dir.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"temp-dir.js","sourceRoot":"","sources":["../../src/lib/temp-dir.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,MAAc,EACd,EAA+B;IAE/B,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC;IACpD,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC;QACH,OAAO,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC;IACvB,CAAC;YAAS,CAAC;QACT,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YAC5D,MAAM,CAAC,IAAI,CAAC,+BAA+B,GAAG,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACzG,CAAC,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAc;IAC9C,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC;IACpD,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC;AACvB,CAAC;AAED,SAAS,cAAc,CAAC,MAAc;IACpC,wEAAwE;IACxE,kEAAkE;IAClE,oEAAoE;IACpE,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IACnD,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACvE,kEAAkE;IAClE,6CAA6C;IAC7C,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC;AAChD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import * as fs from 'node:fs/promises';
|
|
3
|
+
import * as fsSync from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { withTempDir, makeTempDir } from './temp-dir.js';
|
|
7
|
+
describe('temp-dir — withTempDir', () => {
|
|
8
|
+
it('creates a unique directory per call', async () => {
|
|
9
|
+
const dirs = await Promise.all([
|
|
10
|
+
withTempDir('parallel-', async (d) => d),
|
|
11
|
+
withTempDir('parallel-', async (d) => d),
|
|
12
|
+
withTempDir('parallel-', async (d) => d),
|
|
13
|
+
]);
|
|
14
|
+
expect(new Set(dirs).size).toBe(3);
|
|
15
|
+
for (const d of dirs) {
|
|
16
|
+
expect(fsSync.existsSync(d)).toBe(false); // already cleaned
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
it('cleans up after the callback resolves', async () => {
|
|
20
|
+
let captured = '';
|
|
21
|
+
await withTempDir('cleanup-ok-', async (dir) => {
|
|
22
|
+
captured = dir;
|
|
23
|
+
expect(fsSync.existsSync(dir)).toBe(true);
|
|
24
|
+
await fs.writeFile(join(dir, 'x.txt'), 'hi');
|
|
25
|
+
});
|
|
26
|
+
expect(fsSync.existsSync(captured)).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
it('cleans up even when the callback throws', async () => {
|
|
29
|
+
let captured = '';
|
|
30
|
+
await expect(withTempDir('cleanup-fail-', async (dir) => {
|
|
31
|
+
captured = dir;
|
|
32
|
+
await fs.writeFile(join(dir, 'y.txt'), 'bye');
|
|
33
|
+
throw new Error('simulated');
|
|
34
|
+
})).rejects.toThrow('simulated');
|
|
35
|
+
expect(fsSync.existsSync(captured)).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
it('creates a subdirectory under os.tmpdir()', async () => {
|
|
38
|
+
await withTempDir('under-tmpdir-', async (dir) => {
|
|
39
|
+
expect(dir.startsWith(tmpdir())).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
it('sanitizes unsafe characters in the prefix (no traversal literal)', async () => {
|
|
43
|
+
await withTempDir('../../etc/passwd', async (dir) => {
|
|
44
|
+
// mkdtemp + join(tmpdir(), ...) already prevent real traversal; we
|
|
45
|
+
// additionally scrub `..` out of the literal segment for tidy
|
|
46
|
+
// audit logs.
|
|
47
|
+
expect(dir.startsWith(tmpdir())).toBe(true);
|
|
48
|
+
expect(dir.includes('..')).toBe(false);
|
|
49
|
+
expect(dir).toMatch(/etc-passwd-/);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
it('returns the value from the callback', async () => {
|
|
53
|
+
const result = await withTempDir('value-', async () => 'ok');
|
|
54
|
+
expect(result).toBe('ok');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe('temp-dir — makeTempDir', () => {
|
|
58
|
+
it('returns a path that exists and can be manually cleaned', async () => {
|
|
59
|
+
const dir = await makeTempDir('manual-');
|
|
60
|
+
try {
|
|
61
|
+
expect(fsSync.existsSync(dir)).toBe(true);
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
//# sourceMappingURL=temp-dir.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"temp-dir.test.js","sourceRoot":"","sources":["../../src/lib/temp-dir.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,KAAK,MAAM,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEzD,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;IACtC,EAAE,CAAC,qCAAqC,EAAE,KAAK,IAAI,EAAE;QACnD,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YAC7B,WAAW,CAAC,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;YACxC,WAAW,CAAC,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;YACxC,WAAW,CAAC,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;SACzC,CAAC,CAAC;QACH,MAAM,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACnC,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;YACrB,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,kBAAkB;QAC9D,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,KAAK,IAAI,EAAE;QACrD,IAAI,QAAQ,GAAG,EAAE,CAAC;QAClB,MAAM,WAAW,CAAC,aAAa,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YAC7C,QAAQ,GAAG,GAAG,CAAC;YACf,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC1C,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,CAAC;QAC/C,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,KAAK,IAAI,EAAE;QACvD,IAAI,QAAQ,GAAG,EAAE,CAAC;QAClB,MAAM,MAAM,CACV,WAAW,CAAC,eAAe,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YACzC,QAAQ,GAAG,GAAG,CAAC;YACf,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,KAAK,CAAC,CAAC;YAC9C,MAAM,IAAI,KAAK,CAAC,WAAW,CAAC,CAAC;QAC/B,CAAC,CAAC,CACH,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QAC/B,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,KAAK,IAAI,EAAE;QACxD,MAAM,WAAW,CAAC,eAAe,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YAC/C,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kEAAkE,EAAE,KAAK,IAAI,EAAE;QAChF,MAAM,WAAW,CAAC,kBAAkB,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YAClD,mEAAmE;YACnE,8DAA8D;YAC9D,cAAc;YACd,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC5C,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACvC,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;QACrC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,KAAK,IAAI,EAAE;QACnD,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC;QAC7D,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;IACtC,EAAE,CAAC,wDAAwD,EAAE,KAAK,IAAI,EAAE;QACtE,MAAM,GAAG,GAAG,MAAM,WAAW,CAAC,SAAS,CAAC,CAAC;QACzC,IAAI,CAAC;YACH,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5C,CAAC;gBAAS,CAAC;YACT,MAAM,EAAE,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACrD,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL safety guard for any tool that fetches/navigates user-supplied URLs.
|
|
3
|
+
*
|
|
4
|
+
* Blocks four classes of SSRF abuse that an AI assistant can stumble into
|
|
5
|
+
* when the server is asked to open arbitrary URLs:
|
|
6
|
+
* 1. Non-http(s) schemes (file://, ftp://, gopher://, data: etc.)
|
|
7
|
+
* 2. Loopback + link-local + RFC1918 + cloud metadata endpoints
|
|
8
|
+
* 3. IPv6-mapped IPv4 (::ffff:127.0.0.1) that slips past IPv4 regex
|
|
9
|
+
* 4. DNS-rebinding — hostnames that resolve to internal IPs
|
|
10
|
+
*
|
|
11
|
+
* Three entry points:
|
|
12
|
+
* • guardUrl(raw) sync — scheme + literal-host check
|
|
13
|
+
* • resolveAndGuardUrl(raw) async — adds DNS.lookup(family:0) on the host
|
|
14
|
+
* • guardFinalUrl(raw) sync — post-redirect check, reuses guardUrl
|
|
15
|
+
*
|
|
16
|
+
* Opt-in escape hatch for local dev: set MCP_VIDEO_ALLOW_INTERNAL=1.
|
|
17
|
+
*/
|
|
18
|
+
export type UrlGuardResult = {
|
|
19
|
+
ok: true;
|
|
20
|
+
url: string;
|
|
21
|
+
} | {
|
|
22
|
+
ok: false;
|
|
23
|
+
reason: string;
|
|
24
|
+
};
|
|
25
|
+
export declare function guardUrl(raw: unknown): UrlGuardResult;
|
|
26
|
+
/**
|
|
27
|
+
* Async variant that also resolves the hostname via DNS and checks every
|
|
28
|
+
* returned IP against the block list. Catches the simple rebind case where
|
|
29
|
+
* a public hostname resolves to a loopback or RFC1918 address.
|
|
30
|
+
*
|
|
31
|
+
* NOTE: This does not eliminate TOCTOU windows — the browser/ffmpeg will
|
|
32
|
+
* resolve again at request time. But it blocks the trivial path and forces
|
|
33
|
+
* an attacker to rely on narrow TTL-based flipping.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveAndGuardUrl(raw: unknown): Promise<UrlGuardResult>;
|
|
36
|
+
/**
|
|
37
|
+
* Post-navigation check: after `page.goto()` the browser may have followed
|
|
38
|
+
* redirects to an internal host. Pass `page.url()` through this to confirm
|
|
39
|
+
* the final URL is still guard-clean.
|
|
40
|
+
*/
|
|
41
|
+
export declare function guardFinalUrl(raw: unknown): UrlGuardResult;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL safety guard for any tool that fetches/navigates user-supplied URLs.
|
|
3
|
+
*
|
|
4
|
+
* Blocks four classes of SSRF abuse that an AI assistant can stumble into
|
|
5
|
+
* when the server is asked to open arbitrary URLs:
|
|
6
|
+
* 1. Non-http(s) schemes (file://, ftp://, gopher://, data: etc.)
|
|
7
|
+
* 2. Loopback + link-local + RFC1918 + cloud metadata endpoints
|
|
8
|
+
* 3. IPv6-mapped IPv4 (::ffff:127.0.0.1) that slips past IPv4 regex
|
|
9
|
+
* 4. DNS-rebinding — hostnames that resolve to internal IPs
|
|
10
|
+
*
|
|
11
|
+
* Three entry points:
|
|
12
|
+
* • guardUrl(raw) sync — scheme + literal-host check
|
|
13
|
+
* • resolveAndGuardUrl(raw) async — adds DNS.lookup(family:0) on the host
|
|
14
|
+
* • guardFinalUrl(raw) sync — post-redirect check, reuses guardUrl
|
|
15
|
+
*
|
|
16
|
+
* Opt-in escape hatch for local dev: set MCP_VIDEO_ALLOW_INTERNAL=1.
|
|
17
|
+
*/
|
|
18
|
+
import { lookup } from 'node:dns/promises';
|
|
19
|
+
const ALLOWED_SCHEMES = new Set(['https:', 'http:']);
|
|
20
|
+
const BLOCKED_HOST_PATTERNS = [
|
|
21
|
+
/^localhost$/i,
|
|
22
|
+
/^(?:127|10)\./,
|
|
23
|
+
/^192\.168\./,
|
|
24
|
+
/^172\.(?:1[6-9]|2\d|3[0-1])\./,
|
|
25
|
+
/^169\.254\./, // link-local + AWS/GCP/Azure metadata (169.254.169.254)
|
|
26
|
+
/^0\./,
|
|
27
|
+
/^::1$/,
|
|
28
|
+
// IPv6 unique-local is fc00::/7 — that covers fc00..fdff.
|
|
29
|
+
/^f[cd][0-9a-f]{2}:/i,
|
|
30
|
+
/^fe80:/i, // IPv6 link-local
|
|
31
|
+
];
|
|
32
|
+
// IPv4 dotted-quad literal (captures the form the OS + URL parser canonicalize to).
|
|
33
|
+
const IPV4_LITERAL = /^\d{1,3}(?:\.\d{1,3}){3}$/;
|
|
34
|
+
// IPv6-mapped IPv4 in dotted form: ::ffff:127.0.0.1
|
|
35
|
+
const IPV6_MAPPED_IPV4_DOTTED = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/i;
|
|
36
|
+
// IPv6-mapped IPv4 in compact hex form: ::ffff:7f00:1
|
|
37
|
+
const IPV6_MAPPED_IPV4_HEX = /^::ffff:[0-9a-f]{1,4}:[0-9a-f]{1,4}$/i;
|
|
38
|
+
// IPv6-mapped IPv4 in fully uncompressed form: 0:0:0:0:0:ffff:7f00:1
|
|
39
|
+
const IPV6_MAPPED_IPV4_FULL = /^0:0:0:0:0:ffff:[0-9a-f]{1,4}:[0-9a-f]{1,4}$/i;
|
|
40
|
+
function normalizeHost(hostname) {
|
|
41
|
+
// URL parser strips brackets from IPv6 hostnames already; be defensive
|
|
42
|
+
// in case a caller passes the raw host string.
|
|
43
|
+
const unbracketed = hostname.replace(/^\[/, '').replace(/\]$/, '');
|
|
44
|
+
const mapped = unbracketed.match(IPV6_MAPPED_IPV4_DOTTED);
|
|
45
|
+
if (mapped)
|
|
46
|
+
return mapped[1]; // re-check dotted form against IPv4 patterns
|
|
47
|
+
return unbracketed;
|
|
48
|
+
}
|
|
49
|
+
function isMappedIpv4(host) {
|
|
50
|
+
return (IPV6_MAPPED_IPV4_DOTTED.test(host) ||
|
|
51
|
+
IPV6_MAPPED_IPV4_HEX.test(host) ||
|
|
52
|
+
IPV6_MAPPED_IPV4_FULL.test(host));
|
|
53
|
+
}
|
|
54
|
+
function isBlockedHost(hostname) {
|
|
55
|
+
const normalized = normalizeHost(hostname);
|
|
56
|
+
if (isMappedIpv4(normalized)) {
|
|
57
|
+
return `host ${hostname} is IPv6-mapped IPv4 — blocked`;
|
|
58
|
+
}
|
|
59
|
+
for (const pat of BLOCKED_HOST_PATTERNS) {
|
|
60
|
+
if (pat.test(normalized)) {
|
|
61
|
+
return `host ${hostname} is private or loopback — set MCP_VIDEO_ALLOW_INTERNAL=1 to override`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
export function guardUrl(raw) {
|
|
67
|
+
if (typeof raw !== 'string' || raw.length === 0) {
|
|
68
|
+
return { ok: false, reason: 'url must be a non-empty string' };
|
|
69
|
+
}
|
|
70
|
+
let parsed;
|
|
71
|
+
try {
|
|
72
|
+
parsed = new URL(raw);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return { ok: false, reason: 'url is not a valid URL' };
|
|
76
|
+
}
|
|
77
|
+
if (!ALLOWED_SCHEMES.has(parsed.protocol)) {
|
|
78
|
+
return { ok: false, reason: `scheme ${parsed.protocol} is not allowed — use http(s)` };
|
|
79
|
+
}
|
|
80
|
+
if (process.env.MCP_VIDEO_ALLOW_INTERNAL === '1') {
|
|
81
|
+
return { ok: true, url: parsed.toString() };
|
|
82
|
+
}
|
|
83
|
+
const blocked = isBlockedHost(parsed.hostname);
|
|
84
|
+
if (blocked)
|
|
85
|
+
return { ok: false, reason: blocked };
|
|
86
|
+
return { ok: true, url: parsed.toString() };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Async variant that also resolves the hostname via DNS and checks every
|
|
90
|
+
* returned IP against the block list. Catches the simple rebind case where
|
|
91
|
+
* a public hostname resolves to a loopback or RFC1918 address.
|
|
92
|
+
*
|
|
93
|
+
* NOTE: This does not eliminate TOCTOU windows — the browser/ffmpeg will
|
|
94
|
+
* resolve again at request time. But it blocks the trivial path and forces
|
|
95
|
+
* an attacker to rely on narrow TTL-based flipping.
|
|
96
|
+
*/
|
|
97
|
+
export async function resolveAndGuardUrl(raw) {
|
|
98
|
+
const first = guardUrl(raw);
|
|
99
|
+
if (!first.ok)
|
|
100
|
+
return first;
|
|
101
|
+
if (process.env.MCP_VIDEO_ALLOW_INTERNAL === '1')
|
|
102
|
+
return first;
|
|
103
|
+
const parsed = new URL(first.url);
|
|
104
|
+
const host = parsed.hostname.replace(/^\[/, '').replace(/\]$/, '');
|
|
105
|
+
// Literal IPv4/IPv6 — guardUrl already checked, nothing to resolve.
|
|
106
|
+
if (IPV4_LITERAL.test(host) || host.includes(':'))
|
|
107
|
+
return first;
|
|
108
|
+
try {
|
|
109
|
+
const addresses = await lookup(host, { all: true, family: 0 });
|
|
110
|
+
for (const addr of addresses) {
|
|
111
|
+
const blocked = isBlockedHost(addr.address);
|
|
112
|
+
if (blocked) {
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
reason: `host ${host} resolves to private address ${addr.address} — blocked`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
122
|
+
return { ok: false, reason: `DNS lookup failed for ${host}: ${msg}` };
|
|
123
|
+
}
|
|
124
|
+
return first;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Post-navigation check: after `page.goto()` the browser may have followed
|
|
128
|
+
* redirects to an internal host. Pass `page.url()` through this to confirm
|
|
129
|
+
* the final URL is still guard-clean.
|
|
130
|
+
*/
|
|
131
|
+
export function guardFinalUrl(raw) {
|
|
132
|
+
return guardUrl(raw);
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=url-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url-guard.js","sourceRoot":"","sources":["../../src/lib/url-guard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the URL safety guard.
|
|
3
|
+
*
|
|
4
|
+
* The guard is the single chokepoint for any tool that navigates a
|
|
5
|
+
* user-supplied URL (Playwright page.goto, ffmpeg -i http://…). A bug here
|
|
6
|
+
* lets an AI assistant coerce the server to probe localhost, cloud metadata
|
|
7
|
+
* endpoints, or internal RFC1918 addresses, so this file exercises every
|
|
8
|
+
* branch of the reject rules and the MCP_VIDEO_ALLOW_INTERNAL escape hatch.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|