Back to skill

Security audit

pmtools

Security checks for vulnerabilities and agentic risk

Overview

This Feishu OKR skill mostly does what it claims, but it also auto-updates itself and stores tokens in ways users should review before installing.

Install only if you are comfortable with a Feishu OKR tool that can modify OKR data and that also auto-updates its own code. Prefer disabling auto-update, pinning reviewed versions, using trusted HTTPS Feishu endpoints only, and avoiding plaintext token persistence before using it with real organization credentials.

Vulnerability Patterns
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (3)

T03 · Remote Payload Retrieval and Execution

Error
Location
scripts/pm_tools.py:86
Finding
Automatic Unverified Remote Code Replacement<![CDATA[ ## Vulnerability Details **File Location**: `scripts/pm_tools.py:86-117` and `scripts/pm_tools.py:533-535` **Vulnerability Type**: `T03: Remote Payload Retrieval and Execution` **Risk Level**: High ### Vulnerable Code ```python def self_update() -> dict: cache_path = _cache_path() cache = _load_json(cache_path) last_checked = int(cache.get("last_checked_ts", 0) or 0) now = _now_ts() if now - last_checked < 7 * 24 * 60 * 60: return {"skipped": True, "reason": "checked_within_7_days", "version": _read_version()} skill_dir = _skill_dir() updated = False update_attempts: List[Dict[str, Any]] = [] if os.path.isdir(os.path.join(skill_dir, ".git")): rc, out = _run(["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], cwd=skill_dir) if rc == 0 and out: _run(["git", "fetch", "--all", "--prune"], cwd=skill_dir) rc2, out2 = _run(["git", "rev-parse", "HEAD"], cwd=skill_dir) rc3, out3 = _run(["git", "rev-parse", out], cwd=skill_dir) if rc2 == 0 and rc3 == 0 and out2 and out3 and out2 != out3: rc4, out4 = _run(["git", "pull", "--ff-only"], cwd=skill_dir) updated = rc4 == 0 update_attempts.append({"type": "git", "updated": updated, "output": out4}) else: update_attempts.append({"type": "git", "updated": False, "output": "no_update"}) else: update_attempts.append({"type": "git", "updated": False, "output": "no_upstream"}) clawhub_slug = os.environ.get("PM_TOOLS_CLAWHUB_SLUG", "pmtools") rc, out = _run(["clawhub", "update", clawhub_slug]) if rc == 0: updated = True update_attempts.append({"type": "clawhub", "updated": rc == 0, "output": out}) ``` The update is invoked automatically before ordinary commands: ```python try: if args.cmd != "self-update" and os.environ.get("PM_TOOLS_DISABLE_AUTO_UPDATE", "").strip() != "1" ...[truncated 2316 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove automatic updates from the normal command path. Feishu operations must run the locally reviewed version without first modifying it. 2. Make updates an explicit administrative operation requiring informed user confirmation. 3. Pin updates to immutable version identifiers and verified commit hashes or package digests. 4. Require cryptographically signed releases and verify signatures against a bundled, trusted public key before installation. 5. Allowlist the expected repository and package identity rather than trusting ambient Git configuration or an environment-controlled slug. 6. Download updates into a staging directory, verify all files, and activate them atomically only after validation. 7. Do not execute newly installed code in the same operation that retrieves it. 8. Record the verified version and digest in audit logs without recording credentials. 9. Retain `PM_TOOLS_DISABLE_AUTO_UPDATE` only as defense in depth; security must not depend on users setting an opt-out variable. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/pm_tools.py:14
Finding
Credentials and Bearer Tokens Can Be Sent to Arbitrary Plaintext Endpoints<![CDATA[ ## Vulnerability Details **File Location**: `scripts/pm_tools.py:14-15`, `scripts/pm_tools.py:163-166`, and `scripts/pm_tools.py:225-243` **Vulnerability Type**: `T09: Insecure Skill Coding Practices` **Risk Level**: High ### Vulnerable Code ```python BASE_URL = os.environ.get("FEISHU_OKR_BASE_URL", "https://open.feishu.cn/open-apis/okr/v1").rstrip("/") OPEN_API_BASE_URL = os.environ.get("FEISHU_OPEN_API_BASE_URL", "https://open.feishu.cn/open-apis").rstrip("/") ``` The application secret is sent to the configurable authentication origin: ```python def _fetch_tenant_access_token(app_id: str, app_secret: str) -> Tuple[str, int]: url = OPEN_API_BASE_URL + "/auth/v3/tenant_access_token/internal" payload = _request_raw("POST", url, json_body={"app_id": app_id, "app_secret": app_secret}) if not isinstance(payload, dict) or payload.get("code") != 0: raise RuntimeError(json.dumps(payload, ensure_ascii=False)) ``` Bearer tokens are sent to the independently configurable OKR API origin: ```python def _request( method: str, path: str, token: str, query: Optional[Dict[str, Any]] = None, json_body: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None, body_bytes: Optional[bytes] = None, ) -> Dict[str, Any]: url = BASE_URL + path if query: url += "?" + urllib.parse.urlencode(query, doseq=True) h = {"Authorization": f"Bearer {token}"} if headers: h.update(headers) ``` ### Technical Analysis Both API origins are accepted directly from environment variables. The implementation does not require HTTPS, verify that the destination hostname is an approved Feishu domain, or otherwise bind sensitive credentials to their intended recipient. Consequently, `FEISHU_APP_SECRET` can be included in a JSON request to any configured authentication server, while tenant or user bearer tokens can be included in the `Authorization` header of requests to any configured OKR serve ...[truncated 1928 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. In production, restrict authentication and OKR API destinations to the exact expected Feishu HTTPS origin. 2. Parse URLs with `urllib.parse.urlsplit()` and reject: - Any scheme other than `https`. - Unexpected hostnames or ports. - URLs containing user information. - Ambiguous or malformed hostnames. 3. Maintain separate strict allowlists for the token endpoint and OKR API endpoint. 4. Move custom endpoint support into test-only dependency injection rather than production environment variables. 5. If custom enterprise endpoints are genuinely required, require an explicit high-friction opt-in and a configured hostname allowlist. 6. Ensure authorization headers are never forwarded across cross-origin redirects. Prefer disabling redirects for credential-bearing requests or validating every redirect target. 7. Use normal platform TLS certificate and hostname verification, and do not expose options that disable verification. 8. Add tests proving that HTTP URLs and unapproved hosts are rejected before any credential-bearing request is made. 9. Document the exact approved credential destinations and warn administrators against environment-based redirection. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/pm_tools.py:191
Finding
Tenant Access Token Is Persisted in Plaintext Contrary to the Declared Safety Policy<![CDATA[ ## Vulnerability Details **File Location**: `scripts/pm_tools.py:28-32`, `scripts/pm_tools.py:63-69`, and `scripts/pm_tools.py:191-193` **Related Policy Location**: `SKILL.md:16-18` **Vulnerability Type**: `T09: Insecure Skill Coding Practices` **Risk Level**: Medium ### Vulnerable Code The cache destination can be overridden through the environment: ```python def _token_cache_path() -> str: override = os.environ.get("PM_TOOLS_TOKEN_CACHE_PATH") if override: return override return os.path.join(os.path.expanduser("~"), ".cache", "pmtools", "tenant_token.json") ``` The generic JSON writer does not enforce restrictive permissions: ```python def _save_json(path: str, data: dict) -> None: _ensure_parent_dir(path) tmp = path + ".tmp" with open(tmp, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2, sort_keys=True) os.replace(tmp, path) ``` The tenant token is passed directly to that writer: ```python token, expire = _fetch_tenant_access_token(app_id, app_secret) cache = {"tenant_access_token": token, "expire_at_ts": now + expire} _save_json(cache_path, cache) return token ``` This contradicts the stated policy in `SKILL.md`: ```markdown ## Safety - Never print or persist access tokens. ``` ### Technical Analysis The Skill serializes a live tenant access token into a plaintext JSON file. It does not explicitly create the containing directory with mode `0700`, create the token file with mode `0600`, verify file ownership, reject symbolic links, or use an operating-system credential store. Actual exposure depends on the process umask and filesystem configuration. A permissive umask, shared cache directory, backups, filesystem snapshots, or an unsafe path supplied through `PM_TOOLS_TOKEN_CACHE_PATH` can expose the token to other users or processes. The behavior also violates the Skill's explicit safety guarantee that access tokens will never be persisted, which may cause users ...[truncated 1314 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Prefer in-memory token caching and do not persist access tokens to disk. 2. If cross-process caching is operationally necessary, store tokens in an operating-system credential manager or dedicated secrets service. 3. At minimum: - Create the cache directory with mode `0700`. - Create token files atomically with mode `0600`. - Verify that the file and parent directory are owned by the current user. - Reject symbolic links and non-regular files. - Avoid shared, world-writable, or network-mounted cache locations. 4. Remove or tightly validate `PM_TOOLS_TOKEN_CACHE_PATH`; do not allow arbitrary secret-storage paths in production. 5. Delete cached tokens at expiration and provide a command to purge credentials immediately. 6. Never include token values in errors, logs, update output, or diagnostic reports. 7. Update `SKILL.md` so that its security statements accurately match implementation behavior if persistence cannot be removed. 8. Add automated tests for file modes, ownership checks, symlink rejection, expiration cleanup, and assurance that tokens are not printed. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (23)

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

Critical
Category
Data Flow
Content
req = urllib.request.Request(url=url, method=method, headers=h, data=data)
    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            raw = resp.read()
        return json.loads(raw.decode("utf-8"))
    except urllib.error.HTTPError as e:
Confidence
90% confidence
Finding
This request path sends secrets-derived data to a URL whose base is controlled by environment variables (`FEISHU_OPEN_API_BASE_URL`). If an attacker can influence the environment, tenant credentials can be posted to an arbitrary endpoint, causing credential exfiltration via SSRF-like redirection of authentication traffic.

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

Critical
Category
Data Flow
Content
req = urllib.request.Request(url=url, method=method, headers=h, data=data)
    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            raw = resp.read()
        parsed = json.loads(raw.decode("utf-8"))
        return parsed
Confidence
90% confidence
Finding
API requests use `BASE_URL`, which is also environment-controlled, together with bearer tokens in the Authorization header. If that base URL is overridden to an attacker-controlled host, the skill will transmit access tokens and potentially sensitive OKR data to that host.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
The declared purpose is OKR operations, but the skill also performs automatic self-update, external command execution, and local persistence not disclosed in the primary purpose statement. This hidden behavioral expansion is dangerous because users may invoke a business workflow skill without realizing it can modify itself and run external update logic, creating a supply-chain and execution-trust risk.

Credential Access

High
Category
Privilege Escalation
Content
## Safety

- Never print or persist access tokens.
- Confirm before deleting a progress record or changing a period status.

## Setup
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Self-Modification

High
Category
Rogue Agent
Content
Auto-update is executed automatically before every command (with a 7-day local whitelist cache). To force-run and see the update result, run:

```bash
python3 scripts/pm_tools.py self-update
```

This checks for updates at most once per 7 days (local whitelist cache). If a newer version is detected, it updates first, then proceeds.
Confidence
98% confidence
Finding
Automatic self-update before every command is a self-modification mechanism that changes the code executed at runtime, potentially from an external source. In a token-bearing, network-enabled skill, this is especially dangerous because a compromised update channel or malicious upstream change can immediately alter behavior, access credentials, or execute arbitrary commands without explicit user approval.

Context-Inappropriate Capability

High
Confidence
98% confidence
Finding
The skill runs external binaries (`git`, `clawhub`) despite being an OKR API client. Executing these tools expands attack surface, depends on local PATH and repository state, and can result in fetching and applying untrusted code or metadata unrelated to the user's requested OKR action.

Self-Modification

High
Category
Rogue Agent
Content
return 127, f"command not found: {cmd[0]}"


def self_update() -> dict:
    cache_path = _cache_path()
    cache = _load_json(cache_path)
    last_checked = int(cache.get("last_checked_ts", 0) or 0)
Confidence
98% confidence
Finding
The presence of a `self_update` routine indicates deliberate self-modification capability. In a tool meant for OKR operations, self-modifying behavior materially raises supply-chain and integrity risks because the code can change itself outside the normal deployment and review process.

Self-Modification

High
Category
Rogue Agent
Content
parser = argparse.ArgumentParser(prog="pmtools")
    sub = parser.add_subparsers(dest="cmd", required=True)

    sub.add_parser("self-update")

    p = sub.add_parser("periods-create")
    p.add_argument("--period_rule_id", required=True)
Confidence
94% confidence
Finding
Exposing `self-update` as a first-class CLI command makes code-modifying behavior part of the skill's interface even though it is unrelated to OKR management. This increases the chance the capability is invoked in inappropriate contexts and normalizes unsafe maintenance actions inside the runtime tool.

Description-Behavior Mismatch

High
Confidence
99% confidence
Finding
Every non-update command implicitly triggers self-update unless disabled by environment variable. That means routine OKR reads/writes can unexpectedly cause network fetches and local code modification, violating least surprise and creating a hidden supply-chain execution path.

Self-Modification

High
Category
Rogue Agent
Content
args = parser.parse_args(argv)

    try:
        if args.cmd != "self-update" and os.environ.get("PM_TOOLS_DISABLE_AUTO_UPDATE", "").strip() != "1":
            self_update()
        if args.cmd == "self-update":
            _print(self_update())
Confidence
99% confidence
Finding
This line automatically invokes self-update on all non-update commands, creating hidden self-modification during ordinary operations. That makes the issue more dangerous than a manual update command because users cannot easily avoid the code-changing behavior.

Self-Modification

High
Category
Rogue Agent
Content
try:
        if args.cmd != "self-update" and os.environ.get("PM_TOOLS_DISABLE_AUTO_UPDATE", "").strip() != "1":
            self_update()
        if args.cmd == "self-update":
            _print(self_update())
            return 0
        if args.cmd == "periods-create":
Confidence
90% confidence
Finding
Skill modifies its own code, configuration, or behavior at runtime. Self-modification enables an agent to escalate privileges, disable safety constraints, or install persistent backdoors.

Env Variable Harvesting

High
Category
Data Exfiltration
Content
cls.httpd = _start_server()
        cls.base_url = f"http://127.0.0.1:{cls.httpd.server_port}/open-apis/okr/v1"
        cls.open_api_base = f"http://127.0.0.1:{cls.httpd.server_port}/open-apis"
        cls._orig_env = dict(os.environ)
        os.environ["FEISHU_OKR_BASE_URL"] = cls.base_url
        os.environ["FEISHU_OPEN_API_BASE_URL"] = cls.open_api_base
        os.environ["FEISHU_APP_ID"] = "cli_test"
Confidence
60% confidence
Finding
Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Self-Modification

High
Category
Rogue Agent
Content
finally:
            os.unlink(fp)

    def test_self_update_cache_skip(self):
        with tempfile.TemporaryDirectory() as td:
            cache = os.path.join(td, "c.json")
            now = int(time.time())
Confidence
90% confidence
Finding
Skill modifies its own code, configuration, or behavior at runtime. Self-modification enables an agent to escalate privileges, disable safety constraints, or install persistent backdoors.

Self-Modification

High
Category
Rogue Agent
Content
finally:
            os.unlink(fp)

    def test_self_update_cache_skip(self):
        with tempfile.TemporaryDirectory() as td:
            cache = os.path.join(td, "c.json")
            now = int(time.time())
Confidence
90% confidence
Finding
Skill modifies its own code, configuration, or behavior at runtime. Self-modification enables an agent to escalate privileges, disable safety constraints, or install persistent backdoors.

Self-Modification

High
Category
Rogue Agent
Content
finally:
            os.unlink(fp)

    def test_self_update_cache_skip(self):
        with tempfile.TemporaryDirectory() as td:
            cache = os.path.join(td, "c.json")
            now = int(time.time())
Confidence
90% confidence
Finding
Skill modifies its own code, configuration, or behavior at runtime. Self-modification enables an agent to escalate privileges, disable safety constraints, or install persistent backdoors.

Self-Modification

High
Category
Rogue Agent
Content
finally:
            os.unlink(fp)

    def test_self_update_cache_skip(self):
        with tempfile.TemporaryDirectory() as td:
            cache = os.path.join(td, "c.json")
            now = int(time.time())
Confidence
90% confidence
Finding
Skill modifies its own code, configuration, or behavior at runtime. Self-modification enables an agent to escalate privileges, disable safety constraints, or install persistent backdoors.

Lp3

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding
The skill declares broad capabilities through its behavior (environment access, shell, file I/O, and network use) but does not constrain them with explicit tool scope such as permissions or allowed-tools. That omission increases the attack surface because an agent or wrapper may grant more capability than the user expects, especially combined with auto-update and token-based API access.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def _run(cmd: List[str], cwd: Optional[str] = None) -> Tuple[int, str]:
    try:
        p = subprocess.run(
            cmd,
            cwd=cwd,
            stdout=subprocess.PIPE,
Confidence
90% confidence
Finding
The subprocess call itself uses an argument list rather than a shell string, so classic shell injection risk is reduced. However, in this skill it is part of update logic that executes external programs (`git`, `clawhub`) and contributes to unexpected code-fetching and package modification behavior, which expands the trust boundary and can be abused if the environment or upstream source is compromised.

Description-Behavior Mismatch

Medium
Confidence
97% confidence
Finding
An OKR management skill includes self-update functionality that can modify its own codebase via network and local package/repository operations. This is unrelated to the advertised purpose and creates a software supply-chain risk: compromise of upstream sources or execution context can change the skill code without normal review.

Missing User Warnings

Medium
Confidence
97% confidence
Finding
The self-update path can fetch from git upstreams and invoke `clawhub update`, modifying the local installation without visible user disclosure. Hidden self-modification is especially dangerous in agent skills because users expect API operations, not code changes.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The skill caches tenant access tokens to disk under the user's home directory with no mention of credential persistence. While caching is common, plaintext token storage increases exposure to local compromise, backup leakage, or accidental disclosure, especially on shared systems.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The tool performs update-related network and subprocess activity automatically without clear disclosure or consent at the point of use. In a security-sensitive agent environment, undisclosed side effects make it harder to reason about what code executes and when external systems are contacted.

Natural-Language Policy Violations

Low
Confidence
88% confidence
Finding
The test invokes `user_okrs_list` with `lang="zh_cn"`, which embeds a fixed locale choice in natural-language-related behavior. Under the stated policy, forcing a specific language without offering a choice or documenting a justified region-specific constraint is a policy concern.

Static analysis

No suspicious patterns detected.