mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
fix(security): flag python file upload exfiltration
This commit is contained in:
@@ -1145,6 +1145,64 @@ describe("moderationEngine", () => {
|
||||
expect(result.status).toBe("suspicious");
|
||||
});
|
||||
|
||||
it("flags Python clients that base64-upload local files", () => {
|
||||
const result = runStaticModerationScan({
|
||||
slug: "paddleocr-doc-parsing",
|
||||
displayName: "PaddleOCR Doc Parsing",
|
||||
summary: "Parse documents with a hosted OCR API",
|
||||
frontmatter: {},
|
||||
metadata: {},
|
||||
files: [{ path: "scripts/lib.py", size: 512 }],
|
||||
fileContents: [
|
||||
{
|
||||
path: "scripts/lib.py",
|
||||
content: [
|
||||
"import base64",
|
||||
"import httpx",
|
||||
"from pathlib import Path",
|
||||
"def _load_file_as_base64(file_path: str) -> str:",
|
||||
" path = Path(file_path)",
|
||||
' if not path.is_file(): raise FileNotFoundError("missing")',
|
||||
' return base64.b64encode(path.read_bytes()).decode("utf-8")',
|
||||
"def call(api_url, token, file_path):",
|
||||
" params = {'file': _load_file_as_base64(file_path)}",
|
||||
" headers = {'Authorization': f'token {token}'}",
|
||||
" with httpx.Client(timeout=60) as client:",
|
||||
" return client.post(api_url, json=params, headers=headers)",
|
||||
].join("\n"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.reasonCodes).toContain("suspicious.potential_exfiltration");
|
||||
expect(result.status).toBe("suspicious");
|
||||
});
|
||||
|
||||
it("does not flag local-only Python base64 transforms", () => {
|
||||
const result = runStaticModerationScan({
|
||||
slug: "local-encoder",
|
||||
displayName: "Local Encoder",
|
||||
summary: "Encode files locally",
|
||||
frontmatter: {},
|
||||
metadata: {},
|
||||
files: [{ path: "scripts/encode.py", size: 128 }],
|
||||
fileContents: [
|
||||
{
|
||||
path: "scripts/encode.py",
|
||||
content: [
|
||||
"import base64",
|
||||
"from pathlib import Path",
|
||||
"encoded = base64.b64encode(Path('input.pdf').read_bytes())",
|
||||
"Path('encoded.txt').write_bytes(encoded)",
|
||||
].join("\n"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.reasonCodes).not.toContain("suspicious.potential_exfiltration");
|
||||
expect(result.status).toBe("clean");
|
||||
});
|
||||
|
||||
it("does not flag local-only shell base64 transforms", () => {
|
||||
const result = runStaticModerationScan({
|
||||
slug: "local-encoder",
|
||||
|
||||
@@ -115,6 +115,10 @@ const SHELL_BASE64_FILE_READ_PATTERN =
|
||||
/(?:\bcat\s+["']?\$[A-Za-z_][A-Za-z0-9_]*["']?\s*\|\s*base64\b|\bbase64\b[^\n]{0,80}["']?\$[A-Za-z_][A-Za-z0-9_]*["']?)/i;
|
||||
const SHELL_NETWORK_UPLOAD_PATTERN =
|
||||
/\bcurl\b[\s\S]{0,1600}(?:--data(?:-binary|-raw)?\b|-d\b|--form\b|-F\b|--upload-file\b|Authorization\s*:)/i;
|
||||
const PYTHON_BASE64_FILE_READ_PATTERN =
|
||||
/base64\.b64encode\s*\(\s*(?:[A-Za-z_][A-Za-z0-9_]*\.read_bytes\s*\(\s*\)|Path\s*\([^)]*\)\.read_bytes\s*\(\s*\)|open\s*\([^)]*["']rb["'][\s\S]{0,120}\.read\s*\(\s*\))/i;
|
||||
const PYTHON_NETWORK_UPLOAD_PATTERN =
|
||||
/\b(?:requests|session|self\.session|client|httpx\.(?:post|request))\.post\s*\([\s\S]{0,1600}(?:json\s*=|data\s*=|files\s*=|headers\s*=|Authorization)/i;
|
||||
const PLAYWRIGHT_CHROMIUM_PATTERN = /\b(?:playwright\.)?chromium\.launch\s*\(/i;
|
||||
const FILE_URL_BROWSER_NAVIGATION_PATTERN = /\bpage\.goto\s*\([^)]*file:\/\//i;
|
||||
const SVG_HTML_INTERPOLATION_PATTERN =
|
||||
@@ -487,6 +491,12 @@ function findShellBase64FileUpload(content: string) {
|
||||
return findFirstLine(content, SHELL_BASE64_FILE_READ_PATTERN);
|
||||
}
|
||||
|
||||
function findPythonBase64FileUpload(content: string) {
|
||||
if (!/base64\.b64encode/i.test(content)) return null;
|
||||
if (!PYTHON_NETWORK_UPLOAD_PATTERN.test(content)) return null;
|
||||
return findFirstLine(content, PYTHON_BASE64_FILE_READ_PATTERN);
|
||||
}
|
||||
|
||||
function findUnsafeBrowserFileRender(content: string) {
|
||||
if (!PLAYWRIGHT_CHROMIUM_PATTERN.test(content)) return null;
|
||||
if (!FILE_URL_BROWSER_NAVIGATION_PATTERN.test(content)) return null;
|
||||
@@ -840,6 +850,18 @@ function scanCodeFile(
|
||||
});
|
||||
}
|
||||
|
||||
const pythonBase64FileUpload = findPythonBase64FileUpload(content);
|
||||
if (pythonBase64FileUpload) {
|
||||
addFinding(findings, {
|
||||
code: REASON_CODES.EXFILTRATION,
|
||||
severity: "critical",
|
||||
file: path,
|
||||
line: pythonBase64FileUpload.line,
|
||||
message: "Python code base64-encodes a local file and sends it over the network.",
|
||||
evidence: pythonBase64FileUpload.text,
|
||||
});
|
||||
}
|
||||
|
||||
const pythonCredentialPost = findPythonCredentialPostToEnvUrl(content);
|
||||
if (pythonCredentialPost) {
|
||||
addFinding(findings, {
|
||||
|
||||
Reference in New Issue
Block a user