fix(security): flag python file upload exfiltration

This commit is contained in:
Vincent Koc
2026-05-02 22:50:57 -07:00
parent 063ee210a7
commit 46354c9967
2 changed files with 80 additions and 0 deletions
+58
View File
@@ -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",
+22
View File
@@ -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, {