mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(tool): allow standard /dev/* character devices in shell sandbox
This commit is contained in:
parent
b09a220ec7
commit
7272d8f633
@ -9,6 +9,7 @@ import vip.mate.tool.builtin.ToolExecutionContext;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@ -180,6 +181,12 @@ public final class WorkspacePathGuard {
|
||||
// Unparseable as a path — leave it alone, not our concern.
|
||||
continue;
|
||||
}
|
||||
if (isAllowedDeviceNode(normalized)) {
|
||||
// Character devices like /dev/null, /dev/stdin, /dev/fd/0 don't
|
||||
// expose any on-disk user data — allow them so common shell
|
||||
// idioms (`2>/dev/null`, `cmd <(cat file)`) keep working.
|
||||
continue;
|
||||
}
|
||||
if (!normalized.startsWith(root)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Shell command references path outside workspace boundary: "
|
||||
@ -211,6 +218,32 @@ public final class WorkspacePathGuard {
|
||||
private static final Pattern OUTSIDE_ENV_VAR = Pattern.compile(
|
||||
"\\$\\{?(HOME|USER|LOGNAME|TMPDIR|TMP|TEMP|PWD|OLDPWD|PATH|MAIL)\\b");
|
||||
|
||||
/**
|
||||
* Character device nodes that don't expose user data and are needed for
|
||||
* common shell idioms (stderr suppression, process substitution, entropy).
|
||||
* Linux/macOS only — the path strings are absolute POSIX paths; on
|
||||
* Windows {@link #validateShellCommand} doesn't fire on these because
|
||||
* a Windows command wouldn't normalize to a {@code /dev/...} string.
|
||||
*/
|
||||
private static final Set<String> ALLOWED_DEVICE_NODES = Set.of(
|
||||
"/dev/null",
|
||||
"/dev/zero",
|
||||
"/dev/stdin",
|
||||
"/dev/stdout",
|
||||
"/dev/stderr",
|
||||
"/dev/random",
|
||||
"/dev/urandom",
|
||||
"/dev/tty"
|
||||
);
|
||||
|
||||
/** Match {@code /dev/fd/0}, {@code /dev/fd/1}, etc — used by process substitution. */
|
||||
private static final Pattern ALLOWED_DEV_FD = Pattern.compile("^/dev/fd/\\d+$");
|
||||
|
||||
private static boolean isAllowedDeviceNode(Path normalized) {
|
||||
String s = normalized.toString();
|
||||
return ALLOWED_DEVICE_NODES.contains(s) || ALLOWED_DEV_FD.matcher(s).matches();
|
||||
}
|
||||
|
||||
private static String truncateForError(String s) {
|
||||
return s.length() > 200 ? s.substring(0, 200) + "..." : s;
|
||||
}
|
||||
|
||||
@ -172,4 +172,50 @@ class WorkspacePathGuardShellTest {
|
||||
assertDoesNotThrow(() ->
|
||||
WorkspacePathGuard.validateShellCommand("printf '%s\\n' \"$MY_FLAG\""));
|
||||
}
|
||||
|
||||
// ==================== Device-node allowlist ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("/dev/null and other standard device nodes are allowed")
|
||||
void deviceNodes_pass() {
|
||||
assertDoesNotThrow(() ->
|
||||
WorkspacePathGuard.validateShellCommand("find . -name '*.md' 2>/dev/null"));
|
||||
assertDoesNotThrow(() ->
|
||||
WorkspacePathGuard.validateShellCommand("ls -la > /dev/null"));
|
||||
assertDoesNotThrow(() ->
|
||||
WorkspacePathGuard.validateShellCommand("cat /dev/urandom | head -c 16"));
|
||||
assertDoesNotThrow(() ->
|
||||
WorkspacePathGuard.validateShellCommand("dd if=/dev/zero of=zeros.bin bs=1024 count=1"));
|
||||
assertDoesNotThrow(() ->
|
||||
WorkspacePathGuard.validateShellCommand("read line < /dev/stdin"));
|
||||
assertDoesNotThrow(() ->
|
||||
WorkspacePathGuard.validateShellCommand("echo hi > /dev/stderr"));
|
||||
assertDoesNotThrow(() ->
|
||||
WorkspacePathGuard.validateShellCommand("tty < /dev/tty"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("/dev/fd/N (process substitution) is allowed")
|
||||
void devFd_pass() {
|
||||
assertDoesNotThrow(() ->
|
||||
WorkspacePathGuard.validateShellCommand("diff <(sort file_a.txt) <(sort file_b.txt)"));
|
||||
assertDoesNotThrow(() ->
|
||||
WorkspacePathGuard.validateShellCommand("cat /dev/fd/0"));
|
||||
assertDoesNotThrow(() ->
|
||||
WorkspacePathGuard.validateShellCommand("read line < /dev/fd/3"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Non-allowlisted /dev/* paths still rejected")
|
||||
void devOther_blocked() {
|
||||
// Block-device-like paths must not be allowed by the allowlist.
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
WorkspacePathGuard.validateShellCommand("dd if=/dev/disk0 of=image.bin"));
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
WorkspacePathGuard.validateShellCommand("cat /dev/loop0"));
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
WorkspacePathGuard.validateShellCommand("ls /dev/null/sneak"));
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
WorkspacePathGuard.validateShellCommand("cat /dev/fd/notanumber"));
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user