Back to skill

Security audit

Erpclaw Publish 4.15.0 20260816 103527

Security checks across malware telemetry and agentic risk

Overview

ERPClaw is a coherent local ERP skill, but it needs Review because it can change financial and payroll records, manage credentials, and install/update executable modules while some controls and disclosures are under-scoped.

Install only if you intend to let this skill manage real books, payroll, banking metadata, credentials, backups, and local ERP code. Before production use, enable and test RBAC, restrict who can invoke mutating actions, review module provenance before installing industries/add-ons, avoid arbitrary server-path CSV imports, and treat GitHub module/update actions as local code execution.

SkillSpector

By NVIDIA
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (53)

os.system() or os exec-family call

High
Category
Dangerous Code Execution
Content
args[i + 1] = action_override
                break

    os.execvp(sys.executable, [sys.executable, script] + args)


def _suggest_module_for_action(action):
Confidence
84% confidence
Finding
This execvp call forwards execution to a module script located under MODULES_DIR using a module name returned from the database. In this skill's context, expansion modules are installed from GitHub and dynamically dispatched, so if module installation, module metadata, or the modules directory is compromised, this router will execute attacker-controlled Python code with the router's privileges.

eval() call detected

High
Category
Dangerous Code Execution
Content
def __call__(self) -> Any:
        try:
            x = eval(self.arg, globals(), self._dict)

            if isinstance(x, _GetColumns):
                return x.cls
Confidence
95% confidence
Finding
This code evaluates self.arg using Python eval() with module globals and a resolver dictionary. If an attacker can influence relationship expression strings, this can lead to arbitrary code execution during ORM mapper configuration, because eval() can execute arbitrary Python rather than just resolve class names.

exec() call detected

High
Category
Dangerous Code Execution
Content
def attrsetter(attrname):
    code = "def set(obj, value):    obj.%s = value" % attrname
    env = locals().copy()
    exec(code, env)
    return env["set"]
Confidence
93% confidence
Finding
attrsetter() builds Python source code by interpolating attrname directly into an exec() string. If attrname can be influenced by untrusted input, this becomes code injection that can execute arbitrary Python during helper creation; in an agent/plugin ecosystem with many integrations, that risk is more meaningful than in a closed application.

exec() call detected

High
Category
Dangerous Code Execution
Content
env: Dict[str, types.FunctionType] = (
            from_instance is not None and {name: from_instance} or {}
        )
        exec(py, env)
        try:
            env[method].__defaults__ = fn.__defaults__
        except AttributeError:
Confidence
88% confidence
Finding
monkeypatch_proxied_specials() generates and executes Python source using method names and a name expression. While intended for metaprogramming, any attacker influence over those values could lead to code injection or unsafe method creation; in a modular ERP skill with optional extensions, dynamic metaprogramming expands the attack surface.

Dynamic import via __import__()

Medium
Category
Dangerous Code Execution
Content
def register(self, name: str, modulepath: str, objname: str) -> None:
        def load():
            mod = __import__(modulepath)
            for token in modulepath.split(".")[1:]:
                mod = getattr(mod, token)
            return getattr(mod, objname)
Confidence
86% confidence
Finding
PluginLoader.register() performs dynamic imports from a module path string and then resolves an object by name. If an untrusted extension or configuration can supply modulepath/objname, this can load attacker-controlled code and trigger import-time execution; the ERP skill context explicitly supports expansion modules from GitHub, which makes this pattern more dangerous.

eval() call detected

High
Category
Dangerous Code Execution
Content
# is not the usual way variables would resolve.
            cls_namespace.update(base_globals)

            annotation = eval(expression, cls_namespace, locals_)
        else:
            annotation = eval(expression, base_globals, locals_)
    except Exception as err:
Confidence
95% confidence
Finding
This eval() executes annotation strings in the context of module globals and optional class locals, which can lead to arbitrary code execution if an attacker can influence annotation text or modules being introspected. Although this is vendored SQLAlchemy utility code intended for type-hint resolution rather than malicious behavior, using eval on non-constant strings is inherently dangerous in hostile or plugin-rich environments.

eval() call detected

High
Category
Dangerous Code Execution
Content
annotation = eval(expression, cls_namespace, locals_)
        else:
            annotation = eval(expression, base_globals, locals_)
    except Exception as err:
        raise NameError(
            f"Could not de-stringify annotation {expression!r}"
Confidence
95% confidence
Finding
This is the alternate eval() path for resolving stringified annotations outside a class scope, and it carries the same arbitrary code execution risk if expression is attacker-controlled. In an ERP platform with optional GitHub-installed expansion modules, untrusted or insufficiently reviewed plugin code could supply crafted annotations that trigger code execution during introspection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
import tempfile
        tmp_dir = tempfile.mkdtemp(prefix=f"erpclaw-install-{module_name}-")
        try:
            result = subprocess.run(
                ["git", "clone", "--depth", "1", "--filter=blob:none",
                 "--sparse", clone_url, tmp_dir],
                capture_output=True, text=True, timeout=120
Confidence
92% confidence
Finding
This clones and installs code from a GitHub repository specified by the module registry, then later executes module code during installation. Even though the command itself is not shell-injectable, it is part of a trusted-code download pipeline, so compromise of the registry, repo, or maintainer account can lead to arbitrary code execution on the host.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
else:
        # Standalone repo — clone directly
        try:
            result = subprocess.run(
                ["git", "clone", "--depth", "1", clone_url, install_path],
                capture_output=True, text=True, timeout=120
            )
Confidence
92% confidence
Finding
This directly clones a remote repository into the module install path. In this skill's context, module installation is followed by parsing and potentially executing module-supplied code such as init_db.py and migrations, so a malicious or hijacked repo can become arbitrary local code execution.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
env = os.environ.copy()
            lib_path = lib_dir()
            env["PYTHONPATH"] = lib_path + os.pathsep + env.get("PYTHONPATH", "")
            result = subprocess.run(
                [sys.executable, init_db_path],
                capture_output=True, text=True, timeout=60,
                env=env,
Confidence
99% confidence
Finding
The installer executes a module-provided init_db.py from the freshly cloned repository with the host Python interpreter. Because expansion modules are third-party and user-installable from GitHub, this is arbitrary code execution with the privileges of the ERPClaw process and can expose data, modify the system, or establish persistence.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
# Pull latest
        try:
            result = subprocess.run(
                ["git", "pull", "origin", "main"],
                cwd=install_path, capture_output=True, text=True, timeout=60
            )
Confidence
91% confidence
Finding
Updating installed modules via 'git pull' refreshes untrusted remote code in place. Since the update path later re-runs module initialization/migrations, a compromised upstream repository can turn a routine update into host-level arbitrary code execution or destructive data changes.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
init_db_path = os.path.join(install_path, "init_db.py")
        if os.path.isfile(init_db_path):
            try:
                subprocess.run(
                    [sys.executable, init_db_path],
                    capture_output=True, text=True, timeout=60
                )
Confidence
99% confidence
Finding
The update path re-executes module-provided init_db.py after pulling new code, which gives remotely updated modules direct code execution on the host. This is especially risky because users may perceive updates as maintenance rather than execution of newly fetched third-party code.

Tainted flow: 'req' from os.environ.get (line 229, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
base = os.environ.get("ERPCLAW_GITHUB_RAW_BASE", GITHUB_RAW_BASE)
    url = f"{base}/{rel_path}"
    req = urllib.request.Request(url, headers={"User-Agent": "erpclaw"})
    with urllib.request.urlopen(req, timeout=15) as resp:
        data = resp.read()
    actual = hashlib.sha256(data).hexdigest()
    if actual != expected_hash:
Confidence
85% confidence
Finding
The foundation updater honors ERPCLAW_GITHUB_RAW_BASE from the environment to choose where replacement files are downloaded from. Although hashes are checked against the signed manifest, this still lets an attacker who can influence the process environment redirect network traffic to an attacker-controlled host, creating SSRF-style outbound requests and potentially aiding denial-of-service or traffic policy bypass.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill exposes powerful capabilities including shell execution, file read/write, environment access, MCP tool invocation, and limited network access, yet does not declare permissions in a machine-readable way. In a high-impact ERP skill that can modify financial data and install code from GitHub, this mismatch weakens user awareness and platform policy enforcement, increasing the chance of unsafe invocation or over-privileged execution.

Description-Behavior Mismatch

Medium
Confidence
88% confidence
Finding
The README makes a strong privacy/security claim that user data 'never leaves' the machine, yet the documented product behavior includes network retrieval of modules/code from GitHub. Even if business data is not intentionally uploaded, this wording can mislead users about network activity and supply-chain exposure, causing unsafe trust assumptions during installation and runtime expansion.

Intent-Code Divergence

Medium
Confidence
92% confidence
Finding
Saying 'nothing leaves your machine' in the same section that documents GitHub clones/fetches is materially inconsistent and can downplay real outbound network behavior and supply-chain risk. Users may rely on this claim in regulated or air-gapped contexts and approve installation under false assumptions about connectivity and trust boundaries.

Context-Inappropriate Capability

Medium
Confidence
98% confidence
Finding
The UI exposes import actions that ask users for an absolute server-side CSV path rather than using a constrained upload flow. That creates a dangerous trust boundary: a user or prompt-influenced agent could direct backend import logic at arbitrary files on the server, which can enable local file read, unintended processing of sensitive files, or abuse of privileged batch-import capabilities.

Context-Inappropriate Capability

Medium
Confidence
97% confidence
Finding
The supplier import flow also relies on opaque CSV path input, indicating the same unsafe design pattern of trusting a server filesystem location supplied through UI/workflow input. In an ERP context handling financial and vendor data, this broadens the attack surface for arbitrary file access or misuse of backend import routines.

Context-Inappropriate Capability

Low
Confidence
84% confidence
Finding
This file exposes actions that modify company-wide controls such as receipt tolerance and three-way-match policy, which directly affect invoice and receiving validation behavior across the ERP. If callers can invoke buying actions without strong admin-only authorization, they could weaken or disable procurement safeguards and then submit over-receipted or under-matched invoices, enabling fraud or accounting abuse.

Intent-Code Divergence

Medium
Confidence
94% confidence
Finding
The function's docstring states deletion is a soft delete, but the implementation issues a real DELETE against recurring_journal_template. In an ERP/accounting context, operators and downstream automation may rely on the documented behavior for retention, auditability, recoverability, and safe reversibility; this mismatch can cause irreversible loss of scheduling/configuration data and weaken internal controls.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
The file-level policy states the system must never ALTER/DROP tables owned by other modules, and plan_migration enforces ownership checks only during planning. However, rollback_migration later derives table names from stored DDL and unconditionally drops them without revalidating current ownership or scope, so a stale or crafted migration record could cause destructive cross-module table deletion.

Intent-Code Divergence

High
Confidence
98% confidence
Finding
The bank account listing routine masks account numbers but returns fully decrypted routing numbers in the response. Even though routing numbers are less secret than account numbers, exposing full banking details through a generic listing endpoint unnecessarily broadens access to sensitive payroll payment metadata and can facilitate fraud or social-engineering attacks when combined with other employee data.

Context-Inappropriate Capability

Medium
Confidence
87% confidence
Finding
The onboarding workflow can trigger other local skill scripts, including chart-of-accounts setup and demo-data loading, expanding this setup skill's effective authority beyond its nominal scope. In an agent environment, that increases blast radius: a caller invoking onboarding may indirectly execute additional capabilities and side effects that are harder to reason about or authorize.

Context-Inappropriate Capability

Medium
Confidence
90% confidence
Finding
setup_company can auto-install optional industry and regional modules by importing module-management internals and invoking installation routines. In this skill ecosystem, optional modules may come from GitHub, so this creates an elevated supply-chain and capability-expansion risk where a seemingly simple company-setup action can install and activate additional code paths.

Intent-Code Divergence

Medium
Confidence
97% confidence
Finding
The documentation and implementation together reveal a fail-open RBAC design: if RBAC is not active or if no user_id is supplied, permission checks return True. In an ERP context handling accounting, payroll, HR, and financial data, missing or dropped user context can let unauthenticated or unlinked callers perform privileged actions, turning integration mistakes into authorization bypasses.

VirusTotal

VirusTotal findings are pending for this skill version.

View on VirusTotal

Static analysis

Detected: suspicious.dynamic_code_execution, suspicious.insecure_tls_verification

Dynamic code execution detected.

Critical
Code
suspicious.dynamic_code_execution
Location
mcp/server.py:52

Dynamic code execution detected.

Critical
Code
suspicious.dynamic_code_execution
Location
scripts/erpclaw-setup/db_query.py:2613

Dynamic code execution detected.

Critical
Code
suspicious.dynamic_code_execution
Location
scripts/erpclaw-setup/lib/erpclaw_lib/vendor/sqlalchemy/orm/_orm_constructors.py:1142

Dynamic code execution detected.

Critical
Code
suspicious.dynamic_code_execution
Location
scripts/erpclaw-setup/lib/erpclaw_lib/vendor/sqlalchemy/orm/clsregistry.py:533

Dynamic code execution detected.

Critical
Code
suspicious.dynamic_code_execution
Location
scripts/erpclaw-setup/lib/erpclaw_lib/vendor/sqlalchemy/util/typing.py:274

Dynamic code execution detected.

Critical
Code
suspicious.dynamic_code_execution
Location
scripts/erpclaw-setup/migration_runner.py:146

Dynamic code execution detected.

Critical
Code
suspicious.dynamic_code_execution
Location
scripts/module_manager.py:1241

HTTPS certificate verification is disabled.

Warn
Code
suspicious.insecure_tls_verification
Location
scripts/erpclaw-setup/lib/erpclaw_lib/vendor/sqlalchemy/dialects/mysql/pymysql.py:32

HTTPS certificate verification is disabled.

Warn
Code
suspicious.insecure_tls_verification
Location
scripts/erpclaw-setup/lib/erpclaw_lib/vendor/sqlalchemy/dialects/postgresql/pg8000.py:69