Back to skill

Security audit

Content System Feishu Bitable Sync

Security checks for vulnerabilities and agentic risk

Overview

This skill does the Feishu sync it advertises, but it has under-scoped write behavior that could modify Feishu records and write files outside the documented output directory if given a crafted slug.

Review this before installing. Only run it on trusted wechat-report JSON or Markdown, verify the report slug contains only a simple filename-safe value, and expect it to update Feishu Bitable rows plus local status/report files. Avoid using it on shared or untrusted workspaces until slug validation and a confirmation/audit step for record clearing are added.

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 (1)

T09 · Insecure Skill Coding Practices

Error
Location
runtime.py:493
Finding

Unsanitized Report Slug Enables Path Traversal and File Overwrite

Content
View full analysis

Vulnerability Details

File Location: runtime.py:493-503, with file-write sinks at runtime.py:26-28, runtime.py:315-329, and runtime.py:660-674
Vulnerability Type: Path traversal and unintended file overwrite
Risk Level: High

Vulnerable Code

The report slug is read from input-controlled JSON and used directly to construct the manifest path:

python
def run_feishu_bitable_sync(input_path: Path, *, workspace_root: Path) -> dict[str, Any]:
    raw_path, payload = load_raw_payload(input_path)
    request = payload.get("request") or {}
    articles = payload.get("articles") or []
    topic = clean_text(request.get("topic") or raw_path.stem)
    report_slug = clean_text(request.get("slug") or raw_path.stem)

    app_token = require_env("FEISHU_BITABLE_APP_TOKEN")
    table_id = require_env("FEISHU_BITABLE_TABLE_ID")
    auth_mode = resolve_auth_mode()
    manifest_path = workspace_root / "content-production" / "published" / f"{datetime.now().strftime('%Y%m%d')}-{report_slug}-feishu-sync.md"

The same unvalidated slug is used to construct the CSV fallback path:

python
def export_csv_fallback(
    *,
    workspace_root: Path,
    report_slug: str,
    topic: str,
    articles: list[dict[str, Any]],
) -> Path:
    csv_path = workspace_root / "content-production" / "published" / f"{datetime.now().strftime('%Y%m%d')}-{report_slug}-feishu-import.csv"
    csv_path.parent.mkdir(parents=True, exist_ok=True)
    exported_at = datetime.now().isoformat(timespec="seconds")
    with csv_path.open("w", encoding="utf-8-sig", newline="") as handle:
        writer = csv.DictWriter(handle, fieldnames=csv_headers())
        writer.writeheader()
        for article in articles:
            source_url = clean_text(article.get("source_url"))
            if not source_url:
                continue
            writer.writerow(build_csv_row(article, topic=topic, report_slug=report_slug, exported_at=exported_at))
    return csv_path

The generic write ...[truncated 3911 chars]

Remediation
View remediation

Remediation Suggestions

  1. Treat the slug strictly as an identifier rather than a filesystem path. Enforce a conservative allowlist before constructing output paths:
python
SAFE_SLUG = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$")

def validate_slug(value: str) -> str:
    slug = clean_text(value)
    if not SAFE_SLUG.fullmatch(slug):
        raise ValueError(
            "Invalid report slug: only letters, digits, underscores, and hyphens are allowed."
        )
    return slug
  1. Apply validation immediately after reading the input:
python
report_slug = validate_slug(request.get("slug") or raw_path.stem)
  1. Add defense-in-depth path confinement for every generated output:
python
published_root = (
    workspace_root / "content-production" / "published"
).resolve()

manifest_path = (
    published_root
    / f"{datetime.now().strftime('%Y%m%d')}-{report_slug}-feishu-sync.md"
).resolve()

if published_root not in manifest_path.parents:
    raise ValueError("Generated manifest path escapes the publication directory.")

Perform the equivalent check for csv_path.

  1. Explicitly reject absolute paths, .., /, backslashes, drive prefixes, and platform-specific separator variants even if an allowlist is also used.

  2. Avoid silent overwrites where practical. Use exclusive creation, a securely generated unique filename, or an explicit overwrite policy when replacing an existing report is not required.

  3. Add tests covering Unix and Windows traversal forms, repeated traversal components, absolute paths, mixed separators, encoded separators, excessively long slugs, and symlinked output directories.

  4. Consider opening files relative to a trusted directory descriptor with operating-system protections against symlink traversal if the output directory may be writable by untrusted users.

Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (3)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding

The skill declares access to environment variables and local file read/write behavior in its documented operation, but it does not define any explicit tool scope such as permissions or allowed-tools. This creates an overbroad execution surface where an agent may access sensitive local files or secrets (for example Feishu credentials in env vars) without a machine-readable restriction boundary, increasing the risk of unintended data exposure or unsafe execution.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The code can clear an existing Feishu record via clear_record() after moving data into an earlier empty slot, but this destructive behavior is not surfaced here as an explicit user-visible warning, dry-run, or confirmation gate. Because the record selection is driven by table state and source_url matching, a mistake in matching logic or unexpected table contents could silently erase fields from an existing record.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
84% confidence
Finding

The main sync flow sends article data to remote Feishu APIs via POST/PUT requests and also rewrites local JSON/Markdown/CSV files, including status updates to the raw payload. Although these actions are part of synchronization behavior, this file lacks a direct user-facing warning or prompt indicating that running the skill will transmit content externally and modify local files.

Content

No source excerpt is available for this finding.