fix(imessage): sanitize SCP remote path to prevent shell metacharacter injection

References GHSA-g2f6-pwvx-r275.
This commit is contained in:
Robin Waslander
2026-03-14 00:27:58 +01:00
parent ff6636ed5b
commit a54bf71b4c
5 changed files with 149 additions and 3 deletions

View File

@@ -1,5 +1,10 @@
import { describe, expect, it } from "vitest";
import { isSafeScpRemoteHost, normalizeScpRemoteHost } from "./scp-host.js";
import {
isSafeScpRemoteHost,
isSafeScpRemotePath,
normalizeScpRemoteHost,
normalizeScpRemotePath,
} from "./scp-host.js";
describe("scp remote host", () => {
it.each([
@@ -33,3 +38,40 @@ describe("scp remote host", () => {
expect(isSafeScpRemoteHost(value)).toBe(false);
});
});
describe("scp remote path", () => {
it.each([
{
value: "/Users/demo/Library/Messages/Attachments/ab/cd/photo.jpg",
expected: "/Users/demo/Library/Messages/Attachments/ab/cd/photo.jpg",
},
{
value: " /Users/demo/Library/Messages/Attachments/ab/cd/IMG 1234 (1).jpg ",
expected: "/Users/demo/Library/Messages/Attachments/ab/cd/IMG 1234 (1).jpg",
},
])("normalizes safe paths for %j", ({ value, expected }) => {
expect(normalizeScpRemotePath(value)).toBe(expected);
expect(isSafeScpRemotePath(value)).toBe(true);
});
it.each([
null,
undefined,
"",
" ",
"relative/path.jpg",
"/Users/demo/Library/Messages/Attachments/ab/cd/bad$path.jpg",
"/Users/demo/Library/Messages/Attachments/ab/cd/bad`path`.jpg",
"/Users/demo/Library/Messages/Attachments/ab/cd/bad;path.jpg",
"/Users/demo/Library/Messages/Attachments/ab/cd/bad|path.jpg",
"/Users/demo/Library/Messages/Attachments/ab/cd/bad&path.jpg",
"/Users/demo/Library/Messages/Attachments/ab/cd/bad<path.jpg",
"/Users/demo/Library/Messages/Attachments/ab/cd/bad>path.jpg",
'/Users/demo/Library/Messages/Attachments/ab/cd/bad"path.jpg',
"/Users/demo/Library/Messages/Attachments/ab/cd/bad'path.jpg",
"/Users/demo/Library/Messages/Attachments/ab/cd/bad\\path.jpg",
])("rejects unsafe path tokens: %j", (value) => {
expect(normalizeScpRemotePath(value)).toBeUndefined();
expect(isSafeScpRemotePath(value)).toBe(false);
});
});

View File

@@ -1,6 +1,7 @@
const SSH_TOKEN = /^[A-Za-z0-9._-]+$/;
const BRACKETED_IPV6 = /^\[[0-9A-Fa-f:.%]+\]$/;
const WHITESPACE = /\s/;
const SCP_REMOTE_PATH_UNSAFE_CHARS = new Set(["\\", "'", '"', "`", "$", ";", "|", "&", "<", ">"]);
function hasControlOrWhitespace(value: string): boolean {
for (const char of value) {
@@ -60,3 +61,26 @@ export function normalizeScpRemoteHost(value: string | null | undefined): string
export function isSafeScpRemoteHost(value: string | null | undefined): boolean {
return normalizeScpRemoteHost(value) !== undefined;
}
export function normalizeScpRemotePath(value: string | null | undefined): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
if (!trimmed || !trimmed.startsWith("/")) {
return undefined;
}
for (const char of trimmed) {
const code = char.charCodeAt(0);
if (code <= 0x1f || code === 0x7f || SCP_REMOTE_PATH_UNSAFE_CHARS.has(char)) {
return undefined;
}
}
return trimmed;
}
export function isSafeScpRemotePath(value: string | null | undefined): boolean {
return normalizeScpRemotePath(value) !== undefined;
}