From 46354c9967f2c2f33baf1b79bc5c71fea7a7ade0 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 2 May 2026 22:50:57 -0700 Subject: [PATCH] fix(security): flag python file upload exfiltration --- convex/lib/moderationEngine.test.ts | 58 +++++++++++++++++++++++++++++ convex/lib/moderationEngine.ts | 22 +++++++++++ 2 files changed, 80 insertions(+) diff --git a/convex/lib/moderationEngine.test.ts b/convex/lib/moderationEngine.test.ts index ad544fc4..7fc76e44 100644 --- a/convex/lib/moderationEngine.test.ts +++ b/convex/lib/moderationEngine.test.ts @@ -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", diff --git a/convex/lib/moderationEngine.ts b/convex/lib/moderationEngine.ts index f9ac0bd1..59b25f3c 100644 --- a/convex/lib/moderationEngine.ts +++ b/convex/lib/moderationEngine.ts @@ -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, {