mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(tool): enforce workspace boundary on shell commands and file metadata tools
This commit is contained in:
parent
9e9a96f674
commit
b09a220ec7
@ -3,14 +3,15 @@ package vip.mate.tool.builtin;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@ -69,14 +70,28 @@ public class DocumentExtractTool {
|
||||
""")
|
||||
public String extract_document_text(
|
||||
@ToolParam(description = "文件的绝对路径或相对路径") String filePath,
|
||||
@ToolParam(description = "可选参数 JSON,如 {\"pages\": \"1-5\", \"method\": \"tika\"}", required = false) String options) {
|
||||
@ToolParam(description = "可选参数 JSON,如 {\"pages\": \"1-5\", \"method\": \"tika\"}", required = false) String options,
|
||||
// RFC-063r §2.5: hidden from LLM by JsonSchemaGenerator. Carries the
|
||||
// ChatOrigin so the workspace boundary check honors per-agent basePath.
|
||||
@Nullable ToolContext ctx) {
|
||||
|
||||
JSONObject result = new JSONObject();
|
||||
result.set("filePath", filePath);
|
||||
List<String> attempts = new ArrayList<>();
|
||||
|
||||
try {
|
||||
Path path = Paths.get(filePath).toAbsolutePath().normalize();
|
||||
Path path;
|
||||
try {
|
||||
path = vip.mate.tool.guard.WorkspacePathGuard.validatePath(filePath, ctx);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Sandbox rejected the literal path. Try chat-upload basename
|
||||
// resolution before surfacing the boundary error.
|
||||
Path attachment = ChatUploadResolver.resolve(filePath);
|
||||
if (attachment == null) {
|
||||
return errorResult(filePath, e.getMessage(), attempts);
|
||||
}
|
||||
path = attachment;
|
||||
}
|
||||
|
||||
if (!Files.exists(path)) {
|
||||
// The user-uploaded chat attachment is rendered to the LLM as
|
||||
@ -185,10 +200,11 @@ public class DocumentExtractTool {
|
||||
""")
|
||||
public String extract_pdf_text(
|
||||
@ToolParam(description = "PDF 文件的绝对路径或相对路径") String filePath,
|
||||
@ToolParam(description = "页码范围,如 \"1-5\" 或 \"1,3,5\"", required = false) String pages) {
|
||||
@ToolParam(description = "页码范围,如 \"1-5\" 或 \"1,3,5\"", required = false) String pages,
|
||||
@Nullable ToolContext ctx) {
|
||||
|
||||
String options = pages != null ? "{\"pages\": \"" + pages + "\"}" : null;
|
||||
return extract_document_text(filePath, options);
|
||||
return extract_document_text(filePath, options, ctx);
|
||||
}
|
||||
|
||||
@Tool(description = """
|
||||
@ -201,8 +217,9 @@ public class DocumentExtractTool {
|
||||
支持 .docx 和 .doc 格式
|
||||
""")
|
||||
public String extract_docx_text(
|
||||
@ToolParam(description = "Word 文档的绝对路径或相对路径") String filePath) {
|
||||
return extract_document_text(filePath, null);
|
||||
@ToolParam(description = "Word 文档的绝对路径或相对路径") String filePath,
|
||||
@Nullable ToolContext ctx) {
|
||||
return extract_document_text(filePath, null, ctx);
|
||||
}
|
||||
|
||||
// ==================== PDF 提取链 ====================
|
||||
|
||||
@ -3,8 +3,10 @@ package vip.mate.tool.builtin;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
@ -12,7 +14,6 @@ import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
@ -41,13 +42,28 @@ public class FileTypeDetectorTool {
|
||||
注意:对于 .docx/.pdf 等文档,不会返回 read_file,而是 extract_document_text
|
||||
""")
|
||||
public String detect_file_type(
|
||||
@ToolParam(description = "文件的绝对路径或相对路径") String filePath) {
|
||||
@ToolParam(description = "文件的绝对路径或相对路径") String filePath,
|
||||
// RFC-063r §2.5: hidden from LLM by JsonSchemaGenerator. Carries the
|
||||
// ChatOrigin so the workspace boundary check honors per-agent basePath.
|
||||
@Nullable ToolContext ctx) {
|
||||
|
||||
JSONObject result = new JSONObject();
|
||||
result.set("filePath", filePath);
|
||||
|
||||
try {
|
||||
Path path = Paths.get(filePath).toAbsolutePath().normalize();
|
||||
Path path;
|
||||
try {
|
||||
path = vip.mate.tool.guard.WorkspacePathGuard.validatePath(filePath, ctx);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Sandbox rejected the literal path. Fall back to chat-upload
|
||||
// basename matching before surfacing the boundary error — the
|
||||
// LLM may have hallucinated a system path for a real attachment.
|
||||
Path attachment = ChatUploadResolver.resolve(filePath);
|
||||
if (attachment == null) {
|
||||
return errorResult(filePath, e.getMessage());
|
||||
}
|
||||
path = attachment;
|
||||
}
|
||||
|
||||
if (!Files.exists(path)) {
|
||||
// Fall back to chat-upload basename matching for filenames that were
|
||||
|
||||
@ -3,8 +3,10 @@ package vip.mate.tool.builtin;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -51,7 +53,10 @@ public class ShellExecuteTool {
|
||||
+ "Dangerous operations trigger security approval. Returns structured result with exitCode, stdout, stderr, timedOut.")
|
||||
public String execute_shell_command(
|
||||
@ToolParam(description = "Shell command to execute") String command,
|
||||
@ToolParam(description = "Timeout in seconds, default 60", required = false) Integer timeoutSeconds) {
|
||||
@ToolParam(description = "Timeout in seconds, default 60", required = false) Integer timeoutSeconds,
|
||||
// RFC-063r §2.5: hidden from LLM by JsonSchemaGenerator. Carries the
|
||||
// ChatOrigin so the workspace boundary check honors per-agent basePath.
|
||||
@Nullable ToolContext ctx) {
|
||||
|
||||
int timeout = (timeoutSeconds != null && timeoutSeconds > 0) ? timeoutSeconds : DEFAULT_TIMEOUT_SECONDS;
|
||||
// 硬上限:不允许超过 300 秒
|
||||
@ -63,6 +68,21 @@ public class ShellExecuteTool {
|
||||
JSONObject result = new JSONObject();
|
||||
result.set("command", command);
|
||||
|
||||
// Enforce the workspace boundary on the command string itself before
|
||||
// the process starts. The pb.directory() set later only constrains
|
||||
// the CWD — absolute paths in the command would still reach anywhere.
|
||||
try {
|
||||
vip.mate.tool.guard.WorkspacePathGuard.validateShellCommand(command, ctx);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("[ShellExecute] Sandbox rejected command: {}", e.getMessage());
|
||||
result.set("exitCode", -1);
|
||||
result.set("stdout", "");
|
||||
result.set("stderr", e.getMessage());
|
||||
result.set("timedOut", false);
|
||||
result.set("error", e.getMessage());
|
||||
return JSONUtil.toJsonPrettyStr(result);
|
||||
}
|
||||
|
||||
Path stdoutFile = null;
|
||||
Path stderrFile = null;
|
||||
|
||||
|
||||
@ -9,6 +9,8 @@ import vip.mate.tool.builtin.ToolExecutionContext;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 工作区路径沙箱校验器
|
||||
@ -100,6 +102,119 @@ public final class WorkspacePathGuard {
|
||||
return Paths.get(basePath).toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a shell command does not reference filesystem locations
|
||||
* outside the active workspace boundary. When no workspace basePath is
|
||||
* configured, the check is a no-op (matching {@link #validatePath} semantics).
|
||||
*
|
||||
* <p>The check is a static scan of the literal command string. It rejects:
|
||||
* <ul>
|
||||
* <li>any absolute path token (e.g. {@code /etc/passwd}, {@code >/tmp/x},
|
||||
* {@code cd /var}) whose normalized form is not under the workspace
|
||||
* root — even when nested inside command substitution {@code $(...)}
|
||||
* or backticks;</li>
|
||||
* <li>tilde expansion ({@code ~}, {@code ~/...}) — always resolves to
|
||||
* {@code $HOME}, which sits outside the workspace;</li>
|
||||
* <li>references to environment variables ({@code $HOME}, {@code ${USER}},
|
||||
* {@code $TMPDIR}, etc.) that typically resolve outside the workspace.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>Limitations</b> — the static scan is a best-effort defense, not a
|
||||
* true filesystem sandbox. Obfuscated forms ({@code /e''tc/passwd},
|
||||
* variable concatenation like {@code X=/etc; cat $X/passwd}, base64-decoded
|
||||
* paths) can still slip through. The agent is not expected to produce
|
||||
* such forms in normal use, but a fully adversarial caller would need a
|
||||
* real process sandbox (sandbox-exec / firejail / bwrap) on top of this
|
||||
* check.
|
||||
*
|
||||
* @param command the shell command line as it will be passed to {@code sh -c}
|
||||
* @throws IllegalArgumentException when the command references a location
|
||||
* outside the workspace boundary
|
||||
*/
|
||||
public static void validateShellCommand(String command) {
|
||||
validateShellCommand(command, null);
|
||||
}
|
||||
|
||||
/** ToolContext-aware overload — see {@link #validateShellCommand(String)}. */
|
||||
public static void validateShellCommand(String command, @Nullable ToolContext ctx) {
|
||||
if (command == null || command.isEmpty()) return;
|
||||
String basePath = resolveBasePath(ctx);
|
||||
if (basePath == null || basePath.isBlank()) return;
|
||||
Path root = Paths.get(basePath).toAbsolutePath().normalize();
|
||||
|
||||
// 1. Tilde — expands to $HOME, always outside a non-$HOME workspace.
|
||||
if (TILDE_REF.matcher(command).find()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Shell command uses tilde (~) expansion which resolves outside the workspace boundary: "
|
||||
+ truncateForError(command));
|
||||
}
|
||||
|
||||
// 2. Env-var refs to locations that typically resolve outside the workspace.
|
||||
Matcher envMatch = OUTSIDE_ENV_VAR.matcher(command);
|
||||
if (envMatch.find()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Shell command references environment variable " + envMatch.group()
|
||||
+ " which may resolve outside the workspace boundary");
|
||||
}
|
||||
|
||||
// 3. Absolute-path tokens, including those nested inside $(...) or `...`.
|
||||
Matcher pathMatch = ABS_PATH_TOKEN.matcher(command);
|
||||
while (pathMatch.find()) {
|
||||
String candidate = pathMatch.group(1);
|
||||
// Strip trailing punctuation that the shell would treat as a separator
|
||||
// but the regex captured into the path (defensive trim — the character
|
||||
// class excludes most, this catches edge cases like a path followed
|
||||
// by a comma in a sentence).
|
||||
while (candidate.length() > 1) {
|
||||
char tail = candidate.charAt(candidate.length() - 1);
|
||||
if (tail == ',' || tail == ':' || tail == '.' || tail == ')' || tail == ']') {
|
||||
candidate = candidate.substring(0, candidate.length() - 1);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Path normalized;
|
||||
try {
|
||||
normalized = Paths.get(candidate).normalize();
|
||||
} catch (Exception ex) {
|
||||
// Unparseable as a path — leave it alone, not our concern.
|
||||
continue;
|
||||
}
|
||||
if (!normalized.startsWith(root)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Shell command references path outside workspace boundary: "
|
||||
+ normalized + ", allowed root: " + root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Match absolute path tokens — a leading slash that starts a fresh token
|
||||
* (preceded by start-of-string, whitespace, a shell separator, or an
|
||||
* opening quote/parenthesis/backtick) and runs until the next shell
|
||||
* separator or quote. The {@code (?<!:)} lookbehind excludes the second
|
||||
* slash of a URL protocol (e.g. {@code https://host/path}) so URLs aren't
|
||||
* mistaken for filesystem paths.
|
||||
*/
|
||||
private static final Pattern ABS_PATH_TOKEN = Pattern.compile(
|
||||
"(?:^|[\\s|&;<>(`\"'={}])(?<!:)(/[^\\s|&;<>()\"'`{}=]+)");
|
||||
|
||||
/** Bare tilde or tilde at the start of a path token: {@code ~}, {@code ~/foo}, {@code "~/bar"}. */
|
||||
private static final Pattern TILDE_REF = Pattern.compile(
|
||||
"(?:^|[\\s|&;<>(`\"'={}])~(?=[/\\s|&;<>)`\"'$]|$)");
|
||||
|
||||
/**
|
||||
* Env-var references that almost always point outside a project-scoped
|
||||
* workspace. {@code $PATH} is on the list because writing to a directory
|
||||
* on {@code $PATH} is a privilege-escalation vector.
|
||||
*/
|
||||
private static final Pattern OUTSIDE_ENV_VAR = Pattern.compile(
|
||||
"\\$\\{?(HOME|USER|LOGNAME|TMPDIR|TMP|TEMP|PWD|OLDPWD|PATH|MAIL)\\b");
|
||||
|
||||
private static String truncateForError(String s) {
|
||||
return s.length() > 200 ? s.substring(0, 200) + "..." : s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the active workspace base path. Order of preference:
|
||||
* <ol>
|
||||
|
||||
@ -449,7 +449,7 @@ public class WikiRawMaterialService {
|
||||
// 二进制文件:调用 DocumentExtractTool 提取
|
||||
if (entity.getSourcePath() != null && !entity.getSourcePath().isBlank()) {
|
||||
try {
|
||||
String result = documentExtractTool.extract_document_text(entity.getSourcePath(), null);
|
||||
String result = documentExtractTool.extract_document_text(entity.getSourcePath(), null, null);
|
||||
JSONObject json = JSONUtil.parseObj(result);
|
||||
if (json.getBool("success", false)) {
|
||||
String text = json.getStr("text");
|
||||
|
||||
@ -0,0 +1,175 @@
|
||||
package vip.mate.tool.guard;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.junit.jupiter.api.condition.OS;
|
||||
import vip.mate.tool.builtin.ToolExecutionContext;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
/**
|
||||
* Exercises {@link WorkspacePathGuard#validateShellCommand(String)} — the
|
||||
* static command-string scan that backstops {@code ShellExecuteTool} so an
|
||||
* absolute-path reference in the shell command cannot reach outside the
|
||||
* configured workspace boundary, even though the shell process itself has
|
||||
* full filesystem permissions.
|
||||
*
|
||||
* <p>The boundary is read via {@link ToolExecutionContext#workspaceBasePath()}.
|
||||
*/
|
||||
@DisabledOnOs(OS.WINDOWS) // POSIX-style absolute paths in these cases
|
||||
class WorkspacePathGuardShellTest {
|
||||
|
||||
private static final String WORKSPACE = "/tmp/ws-guard-shell-test";
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
ToolExecutionContext.set("conv-test", "test-user", WORKSPACE);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void teardown() {
|
||||
ToolExecutionContext.clear();
|
||||
}
|
||||
|
||||
// ==================== No-op when sandbox absent ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("No workspace configured → all commands pass")
|
||||
void noWorkspace_noop() {
|
||||
ToolExecutionContext.clear();
|
||||
assertDoesNotThrow(() -> WorkspacePathGuard.validateShellCommand("cat /etc/passwd"));
|
||||
assertDoesNotThrow(() -> WorkspacePathGuard.validateShellCommand("rm -rf /"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Null or empty command → no-op")
|
||||
void nullOrEmpty_noop() {
|
||||
assertDoesNotThrow(() -> WorkspacePathGuard.validateShellCommand(null));
|
||||
assertDoesNotThrow(() -> WorkspacePathGuard.validateShellCommand(""));
|
||||
}
|
||||
|
||||
// ==================== In-boundary commands pass ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Relative paths and in-workspace absolute paths pass")
|
||||
void inBoundary_pass() {
|
||||
assertDoesNotThrow(() -> WorkspacePathGuard.validateShellCommand("ls -la"));
|
||||
assertDoesNotThrow(() -> WorkspacePathGuard.validateShellCommand("cat foo.txt"));
|
||||
assertDoesNotThrow(() -> WorkspacePathGuard.validateShellCommand("cat subdir/bar.txt"));
|
||||
assertDoesNotThrow(() -> WorkspacePathGuard.validateShellCommand(
|
||||
"cat " + WORKSPACE + "/foo.txt"));
|
||||
assertDoesNotThrow(() -> WorkspacePathGuard.validateShellCommand(
|
||||
"cd " + WORKSPACE + "/subdir && ls"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("URLs are not mistaken for filesystem paths")
|
||||
void urls_pass() {
|
||||
assertDoesNotThrow(() -> WorkspacePathGuard.validateShellCommand(
|
||||
"curl -s https://example.com/api/data"));
|
||||
assertDoesNotThrow(() -> WorkspacePathGuard.validateShellCommand(
|
||||
"wget -O out.txt http://host:8080/path/to/file"));
|
||||
}
|
||||
|
||||
// ==================== Out-of-boundary absolute paths blocked ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Absolute path outside workspace → rejected")
|
||||
void absoluteOutside_blocked() {
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
WorkspacePathGuard.validateShellCommand("cat /etc/passwd"));
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
WorkspacePathGuard.validateShellCommand("head -3 /Users/someone/code/secret.md"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Output redirection to outside path → rejected")
|
||||
void redirection_blocked() {
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
WorkspacePathGuard.validateShellCommand("echo evil > /tmp/leak.txt"));
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
WorkspacePathGuard.validateShellCommand("ls >> /var/log/sneak.log"));
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
WorkspacePathGuard.validateShellCommand("grep foo < /etc/hosts"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("cd / pushd to outside path → rejected")
|
||||
void cdOutside_blocked() {
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
WorkspacePathGuard.validateShellCommand("cd /etc && ls"));
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
WorkspacePathGuard.validateShellCommand("pushd /var/spool"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ln -s to an outside target → rejected")
|
||||
void symlinkCreate_blocked() {
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
WorkspacePathGuard.validateShellCommand("ln -sf /etc/passwd alias"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Pipe with outside path on either side → rejected")
|
||||
void pipeOutside_blocked() {
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
WorkspacePathGuard.validateShellCommand("cat /etc/passwd | grep root"));
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
WorkspacePathGuard.validateShellCommand("env | tee /tmp/dump.txt"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Quoted absolute path → rejected")
|
||||
void quotedAbsolute_blocked() {
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
WorkspacePathGuard.validateShellCommand("cat \"/etc/passwd\""));
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
WorkspacePathGuard.validateShellCommand("cat '/etc/passwd'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Command substitution $(...) with outside path → rejected")
|
||||
void commandSubstOutside_blocked() {
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
WorkspacePathGuard.validateShellCommand("echo $(cat /etc/passwd)"));
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
WorkspacePathGuard.validateShellCommand("X=`head /etc/hostname`; echo $X"));
|
||||
}
|
||||
|
||||
// ==================== Tilde + env-var rejection ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Tilde expansion → rejected")
|
||||
void tilde_blocked() {
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
WorkspacePathGuard.validateShellCommand("cat ~/.zshrc"));
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
WorkspacePathGuard.validateShellCommand("cd ~ && ls"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("$HOME / ${HOME} / $TMPDIR / $PATH → rejected")
|
||||
void envVarOutside_blocked() {
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
WorkspacePathGuard.validateShellCommand("cat $HOME/.zshrc"));
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
WorkspacePathGuard.validateShellCommand("ls ${HOME}/Documents"));
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
WorkspacePathGuard.validateShellCommand("touch $TMPDIR/leak"));
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
WorkspacePathGuard.validateShellCommand("echo bad > $PATH/evil"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Other env vars not on the deny list are allowed")
|
||||
void unrelatedEnvVar_pass() {
|
||||
assertDoesNotThrow(() ->
|
||||
WorkspacePathGuard.validateShellCommand("echo $LANG"));
|
||||
assertDoesNotThrow(() ->
|
||||
WorkspacePathGuard.validateShellCommand("printf '%s\\n' \"$MY_FLAG\""));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user