Back to skill

Security audit

Scheduled Reports

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its scheduled-report purpose, but it needs review because its validator and recurring delivery model leave local file paths and delivery targets too broadly scoped.

Install only where OpenClaw cron jobs run with least privilege. Keep report definitions and previews in private approved directories, restrict folder delivery to an export root, prefer administrator-approved webhook aliases, and require explicit human approval before enabling or changing any recurring report.

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

Warning
Location
scripts/validate_report_definition.py:435
Finding

Unrestricted file references permit access outside approved report storage

Content
View full analysis

Vulnerability Details

File Location: scripts/validate_report_definition.py:435-443, scripts/validate_report_definition.py:485-500, and scripts/validate_report_definition.py:592-600
Vulnerability Type: Improper path restriction and arbitrary local file reference
Risk Level: Medium

Vulnerable Code

python
def resolve_reference_path(reference: str, input_path: Path) -> Path:
    candidate = Path(reference)
    if candidate.is_absolute():
        return candidate

    cwd_candidate = Path.cwd() / candidate
    if cwd_candidate.exists():
        return cwd_candidate.resolve()
    return (input_path.parent / candidate).resolve()
python
query_file = value.get("queryFile")
if query_file is not None:
    if not is_non_empty_string(query_file):
        add_error(errors, f"{path}.queryFile must be a non-empty string when present")
        return
    query_file_hash = validate_sha256_string(
        data_source.get("queryFileSha256"),
        f"{path.rsplit('.', 1)[0]}.queryFileSha256",
        errors,
    )
    resolved_path = resolve_reference_path(str(query_file).strip(), input_path)
    if not resolved_path.exists() or not resolved_path.is_file():
        add_error(errors, f"{path}.queryFile must point to an existing file: {resolved_path}")
        return
    if query_file_hash is not None:
        actual_hash = sha256_file(resolved_path)
        if actual_hash != query_file_hash:
            add_error(
                errors,
                f"{path.rsplit('.', 1)[0]}.queryFileSha256 does not match the contents of {resolved_path}",
            )
python
def validate_preview(value: object, input_path: Path, errors: list[str]) -> None:
    if not isinstance(value, dict):
        add_error(errors, "definition.preview must be an object when present")
        return
    artifact_ref = validate_required_string(value, "artifactRef", errors, "
...[truncated 2507 chars]
Remediation
View remediation

Remediation Suggestions

  • Define separate, explicit allowlisted roots for query files and preview artifacts.
  • Reject absolute paths supplied by report definitions.
  • Resolve the candidate and configured root with Path.resolve(strict=True), then require candidate.is_relative_to(root).
  • Reject symlinks or verify that the fully resolved target remains beneath the approved root.
  • Require a regular file and apply expected extension or content-type restrictions to preview artifacts.
  • Avoid returning resolved sensitive paths in validation errors exposed to untrusted users.
  • Ensure downstream delivery independently repeats the same canonical-path containment check rather than trusting prior validation.
  • Run validation and report delivery under a minimally privileged service account that cannot read unrelated secrets.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/validate_report_definition.py:330
Finding

Delivery target validation allows DNS-based internal webhooks and unrestricted absolute folder paths

Content
View full analysis

Vulnerability Details

File Location: scripts/validate_report_definition.py:330-360 and scripts/validate_report_definition.py:363-371
Vulnerability Type: Incomplete SSRF prevention and insufficient filesystem destination restriction
Risk Level: Medium

Vulnerable Code

python
def validate_webhook_url(value: object, path: str, errors: list[str]) -> str | None:
    if not is_non_empty_string(value):
        add_error(errors, f"{path} must be a non-empty HTTPS URL")
        return None

    normalized = str(value).strip()
    parsed = urlparse(normalized)
    if parsed.scheme.lower() != "https":
        add_error(errors, f"{path} must use HTTPS")
        return None
    if not parsed.hostname:
        add_error(errors, f"{path} must include a hostname")
        return None

    host = parsed.hostname.lower()
    if host in {"localhost"} or host.endswith(".local"):
        add_error(errors, f"{path} must not target localhost or .local hosts")
        return None

    try:
        ip = ipaddress.ip_address(host)
    except ValueError:
        return normalized

    if any((ip.is_private, ip.is_loopback, ip.is_link_local, ip.is_reserved, ip.is_multicast)):
        add_error(errors, f"{path} must not target private, loopback, link-local, reserved, or multicast IPs")
        return None
    return normalized
python
def validate_folder_path(value: object, path: str, errors: list[str]) -> str | None:
    if not is_non_empty_string(value):
        add_error(errors, f"{path} must be a non-empty folder path")
        return None
    normalized = str(value).strip()
    parts = [part for part in re.split(r"[\\/]+", normalized) if part]
    if ".." in parts:
        add_error(errors, f"{path} must not contain parent-directory traversal")
        return None
    return normalized

Technical Analysis

The webhook check rejects prohibited addresses ...[truncated 2677 chars]

Remediation
View remediation

Remediation Suggestions

  • Prefer configured webhook aliases mapped to administrator-approved endpoints instead of accepting arbitrary URLs.
  • Resolve every hostname before connection and reject the destination if any resolved address is not globally routable.
  • Repeat address validation immediately before connecting to reduce DNS-rebinding risk.
  • Disable redirects or validate every redirect destination using the same DNS and IP rules.
  • Pin the validated destination address for the connection where the HTTP client permits it, while preserving correct TLS hostname verification.
  • Apply egress firewall rules that prevent the report runtime from reaching loopback, private, link-local, metadata, and management networks.
  • Restrict folder delivery to a configured export root. Resolve both the root and destination and require the destination to remain beneath that root.
  • Reject absolute user-controlled folder paths and verify that symlinks cannot escape the approved root.
  • Use least-privilege filesystem permissions and prevent the delivery account from modifying application code, configuration, credentials, or system directories.
Vulnerability Patterns
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • 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
92% confidence
Finding

The skill requires file access and implicitly relies on network-capable delivery mechanisms, but it does not declare an explicit tool or permission scope. That creates an authorization ambiguity where a host may grant broader capabilities than intended, increasing the risk of unintended file exposure or outbound delivery actions during preview generation, validation, or report delivery.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The example caption block mandates French text ("Aperçu du rapport. Répondez OK pour activer.") in workflow instructions, but the document does not offer a user language choice or justify a French-only locale. This creates a natural-language policy concern because the skill can impose a specific language on users by default.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The sample executionPrompt explicitly instructs runtime AI to "Generate the PDF in French," which imposes a specific language choice. The document does not indicate that this language was user-selected or that the report is a region-specific/compliance-driven exception, so it conflicts with the language/locale choice policy.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.