Back to skill

Security audit

Odoo JSON2 Operator

Security checks for vulnerabilities and agentic risk

Overview

This Odoo automation skill is purpose-aligned, but it ships and stores bearer API keys in plaintext and can perform high-impact business data changes.

Review before installing. Treat the bundled Odoo API key as exposed and rotate it. Do not use production credentials with this skill until plaintext profile storage is removed or replaced with a secure secret store, HTTPS is enforced, and cleanup is limited to files the client created under its temp directory.

Vulnerability Patterns
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (2)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/connections.json:1
Finding
Plaintext Odoo bearer credential bundled and persistently stored in the Skill package<![CDATA[ ## Vulnerability Details **File Location**: `scripts/connections.json:1-7`; `scripts/odoo_json2_client.py:64-79`; `SKILL.md:42-45, 116` **Vulnerability Type**: Hardcoded secret and insecure plaintext credential storage **Risk Level**: High ### Vulnerable Code `scripts/connections.json:1-7`: ```json { "odoo19c": { "base_url": "https://odoo19c.ylhctec.com", "database": "odoo19c", "api_key": "[REDACTED: plaintext 40-character API key was present]" } } ``` `scripts/odoo_json2_client.py:64-79`: ```python def _save_profiles(profiles: dict[str, dict[str, str]]) -> None: PROFILE_FILE.write_text( json.dumps(profiles, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) def save_profile(name: str, base_url: str, database: str, api_key: str) -> int: profiles = _load_profiles() profiles[name] = { "base_url": base_url.rstrip("/"), "database": database, "api_key": api_key, } _save_profiles(profiles) print(f"Profile saved: {name}") return 0 ``` `SKILL.md:42-45, 116`: ```markdown - Persist successful connections locally as named profiles containing `base_url`, `database`, and `api_key`. - Reuse saved profile by default in later turns; do not repeatedly ask for credentials. - If multiple profiles exist and user does not specify a target system at session start, ask which system/profile to use before proceeding. - Ask for `base_url`, `database`, and `api_key` only when no usable profile exists or when user wants a new/updated system. ``` ```markdown - Never log or persist API keys in files, command history snippets, or chat output. ``` ### Technical Analysis The distributed `connections.json` contains a plaintext, live-looking bearer credential together with its target host and database. The credential value has been redacted in this report to avoid further disclosure. The profile-saving implementation also serializes future API keys directly into `scripts/connectio ...[truncated 2954 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. **Immediately revoke and rotate the exposed credential.** - Treat the bundled key as compromised. - Review Odoo access logs for unexpected discovery, read, write, delete, or custom-method requests. - Restrict the replacement token to the minimum models and operations required. 2. **Remove credentials from distributed artifacts and repository history.** - Replace `scripts/connections.json` with an empty template. - Add the real profile file to `.gitignore` and package-exclusion rules. - Purge the exposed value from version-control history, caches, release archives, and backups where feasible. 3. **Use a secure credential storage mechanism.** - Prefer an operating-system keychain, managed secret store, or short-lived environment injection. - Store only a secret reference in the profile file. - Avoid accepting secrets directly through command-line arguments because process listings and shell history may expose them. 4. **If file storage is unavoidable:** - Store profiles outside the source and installation tree. - Create the file with owner-only permissions, such as mode `0600` on POSIX systems. - Validate ownership and permissions before reading it. - Clearly warn users that plaintext storage is being used. 5. **Enforce secure transport.** - Parse and validate `base_url`. - Require the `https` scheme. - Permit plaintext HTTP only through an explicit development-only override restricted to loopback addresses. 6. **Make the documentation consistent.** - Remove the instruction to persist raw API keys, or revise the implementation to use secure storage. - Preserve the existing guardrail prohibiting secrets in files, command history, and chat output. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/odoo_json2_client.py:145
Finding
Automatic payload cleanup can delete JSON files outside the designated temporary directory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/odoo_json2_client.py:145-170, 278-285, 294-301` **Vulnerability Type**: Unsafe temporary-file cleanup and excessive filesystem scope **Risk Level**: Medium ### Vulnerable Code `scripts/odoo_json2_client.py:145-170`: ```python def is_cleanup_candidate(path: Path, tmp_dir: Path) -> bool: if path.suffix.lower() != ".json": return False if path.name.startswith("tmp_"): return True try: path.relative_to(tmp_dir.resolve()) return True except ValueError: return False def load_payload( payload: str | None, payload_file: str | None, cleanup_tmp_payload_file: bool, tmp_dir: Path, ) -> tuple[dict[str, Any], Path | None]: if payload and payload_file: raise ValueError("Use either --payload or --payload-file, not both.") cleanup_file: Path | None = None if payload_file: payload_path = resolve_payload_path(payload_file, tmp_dir) with payload_path.open("r", encoding="utf-8") as handle: data = json.load(handle) if cleanup_tmp_payload_file and is_cleanup_candidate(payload_path, tmp_dir): cleanup_file = payload_path ``` `scripts/odoo_json2_client.py:278-285`: ```python tmp_dir = Path(args.tmp_dir).resolve() tmp_dir.mkdir(parents=True, exist_ok=True) cleanup_tmp_payload_file = args.cleanup_tmp_payload_file and not args.keep_tmp_payload_file payload, cleanup_file = load_payload( args.payload, args.payload_file, cleanup_tmp_payload_file, tmp_dir, ) ``` `scripts/odoo_json2_client.py:294-301`: ```python finally: if cleanup_file and cleanup_file.exists(): try: cleanup_file.unlink() except OSError as err: print(f"WARN: failed to remove temp payload file {cleanup_file}: {err}", file=sys.stderr) ``` ### Technical Analysis The cleanup policy considers any JSON file whose basename begins with `tmp_` to be disposable: ``` ...[truncated 2306 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. **Restrict automatic deletion to the configured temporary directory.** - Resolve both the candidate path and temporary directory. - Require the candidate to be strictly contained within `tmp_dir`. - Remove the global `tmp_` filename rule. Example hardened logic: ```python def is_cleanup_candidate(path: Path, tmp_dir: Path) -> bool: candidate = path.resolve() root = tmp_dir.resolve() if candidate.suffix.lower() != ".json": return False try: candidate.relative_to(root) except ValueError: return False return candidate != root ``` 2. **Only delete files created or explicitly registered by the client.** - Track temporary files at creation time. - Do not automatically delete arbitrary caller-supplied paths. - Prefer `tempfile.NamedTemporaryFile` or a similarly safe API. 3. **Make deletion explicit for external files.** - Treat `--payload-file` as caller-owned by default. - Add an explicit opt-in flag if users genuinely need external-file deletion. - Display the resolved path before deletion when interactive confirmation is possible. 4. **Harden against path and link ambiguity.** - Resolve paths before containment checks. - Reject symlinks for automatically managed payloads where appropriate. - Ensure that cleanup cannot follow a path outside the temporary root. 5. **Preserve failed-request evidence safely.** - Consider deleting an internally created temporary payload only after successful completion. - If deletion after failure remains necessary, document that behavior clearly and limit it to client-created files. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (9)

Intent-Code Divergence

High
Confidence
98% confidence
Finding
The skill instructs the agent to persist successful connections as profiles containing `base_url`, `database`, and `api_key`, while later guardrails say API keys must never be logged or persisted. Persisting bearer credentials locally creates a direct secret-retention risk: theft of the profile store or unintended reuse could grant ongoing access to the Odoo instance.

Missing User Warnings

High
Confidence
96% confidence
Finding
The skill tells the agent to save and reuse connection profiles containing an API key, but it does not clearly warn the user that credentials may be retained locally. That lack of disclosure undermines informed consent and can lead users to reveal production secrets without understanding the storage and reuse behavior.

Lp3

Medium
Category
MCP Least Privilege
Confidence
86% confidence
Finding
The skill enables file read, file write, and network actions but does not declare any explicit tool scope or permissions boundary. That omission increases the chance of over-broad invocation and makes it harder for reviewers or runtime policy to constrain sensitive operations such as credential handling and outbound API access.

Vague Triggers

Medium
Confidence
89% confidence
Finding
The invocation description is broad enough to match generic requests to query, mutate, or automate business actions against Odoo. In a skill that can perform authenticated network calls and data mutations, overly broad routing can cause accidental activation for ambiguous user prompts and raise the chance of unauthorized or unsafe operations.

Natural-Language Policy Violations

Medium
Confidence
98% confidence
Finding
The workflow requires analysis and reports to be produced 'in plain Chinese,' and later repeats 'Use plain Chinese,' without indicating user opt-in or a region-specific constraint. This imposes a language choice unconditionally and can violate language/locale policy expectations.

Ssd 3

Medium
Confidence
97% confidence
Finding
The instructions explicitly direct persistence and automatic reuse of user-supplied connection credentials, including bearer API keys. In the context of an Odoo operator with network and mutation capability, retaining and silently reusing credentials materially increases the risk of secret leakage, cross-task misuse, and unintended actions against the wrong environment.

Natural-Language Policy Violations

Medium
Confidence
97% confidence
Finding
The markdown explicitly instructs the skill to 'Use plain Chinese' for analysis output, with no option for another language. This is a natural-language policy issue because it enforces a locale/language choice without opt-in or justification.

Natural-Language Policy Violations

Medium
Confidence
93% confidence
Finding
The playbook mandates a fixed output structure using Chinese headings such as '结论摘要' and '关键发现'. This imposes a specific language on responses without documenting user preference, opt-in, or a justified locale-specific constraint, which is a natural-language policy concern.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The script persists bearer API keys in a local JSON profile file in plaintext, with no warning to the user and no file-permission hardening. If the workstation, repository, backups, or shared filesystem are accessible to other users or processes, the stored token can be recovered and used to query or mutate Odoo data through the JSON-2 API.