Back to skill

Security audit

Meta Ads Control

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its Meta Ads purpose, but its script can send a Meta bearer token to arbitrary URLs despite documenting graph.facebook.com as the expected network scope.

Review this skill before installing. It is useful for real Meta Ads operations and includes good confirmation guidance for spend or delivery changes, but only use it with tightly scoped Meta tokens and do not use custom META_GRAPH_BASE values or full request URLs unless the script is fixed to allow only trusted Meta HTTPS hosts. Rotate any token used with untrusted full URLs, custom graph bases, or unexpected pagination sources.

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
scripts/meta_ads.py:322
Finding
Meta Access Token Can Be Disclosed to Arbitrary Network Destinations## Vulnerability Details **File Location**: `scripts/meta_ads.py:322-365`, with exploitable call paths at `scripts/meta_ads.py:500-519`, `scripts/meta_ads.py:693-705`, and configurable endpoint input at `scripts/meta_ads.py:902-905` **Vulnerability Type**: Unrestricted authenticated request destination and credential disclosure **Risk Level**: High ### Vulnerable Code The URL builder accepts absolute HTTP and HTTPS URLs without validating their destination: ```python def build_url(self, path: str) -> str: if path.startswith("http://") or path.startswith("https://"): return path clean = path.lstrip("/") base = self.graph_base.rstrip("/") if not clean: return f"{base}/{self.api_version}" if clean.startswith(f"{self.api_version}/"): return f"{base}/{clean}" return f"{base}/{self.api_version}/{clean}" ``` The Meta bearer token is then attached unconditionally to the resulting URL: ```python def request( self, method: str, path: str, params: Optional[Dict[str, Any]] = None, files: Optional[Dict[str, Tuple[str, bytes, str]]] = None, ) -> Dict[str, Any]: method = method.upper() url = self.build_url(path) encoded_params = normalise_params(params) data_bytes: Optional[bytes] = None headers: Dict[str, str] = { "Authorization": f"Bearer {self.access_token}", "User-Agent": USER_AGENT, } if method in {"GET", "HEAD"} and not files: url = append_query(url, encoded_params) elif files: body, content_type = build_multipart(encoded_params, files) headers["Content-Type"] = content_type data_bytes = body else: headers["Content-Type"] = "application/x-www-form-urlencoded" data_bytes = urllib.parse.urlencode(encoded_params).encode("utf-8") if encoded_params else b"" last_payload: Any = None last_headers: Dict[str, str] = {} for attempt in range(self.max_retries + 1): req = urllib.request.Req ...[truncated 5647 chars]
Remediation
## Remediation Suggestions 1. **Restrict authenticated requests to trusted Meta origins** - Parse every final URL with `urllib.parse.urlsplit()`. - Require the `https` scheme. - Allow only an explicit hostname allowlist, normally `graph.facebook.com`. - Reject embedded credentials, unexpected ports, malformed hostnames, and non-HTTPS URLs. 2. **Reject absolute user-supplied request paths** - Make the low-level `request` command accept only relative Graph API paths. - Resolve those paths against a validated, fixed Meta Graph API base. - Do not treat `http://` or `https://` values as valid API paths. 3. **Constrain or remove custom Graph bases** - Remove `--graph-base` and `META_GRAPH_BASE` if they are not operationally required. - If custom endpoints are required for testing, require an explicit unsafe-development mode and never reuse production `META_ACCESS_TOKEN` credentials with them. - Prefer a separate test credential for custom Graph-compatible endpoints. 4. **Validate pagination URLs** - Parse each `paging.next` value before following it. - Require it to use HTTPS and match the original validated Meta origin. - Prefer extracting the relative path and approved query parameters rather than directly requesting the supplied absolute URL. 5. **Harden redirect handling** - Disable automatic redirects or implement a redirect handler that revalidates every destination. - Never forward the `Authorization` header when the scheme, hostname, or port changes. - Reject HTTPS-to-HTTP redirects unconditionally. 6. **Add security regression tests** - Verify rejection of arbitrary absolute URLs. - Verify rejection of plain HTTP, foreign hosts, foreign ports, hostname confusion, and cross-origin redirects. - Verify that malicious `paging.next` values are not followed. - Verify that credentials are only attached after destination validation. 7. **Operational response** - Rotate any token that may have been used ...[truncated 214 chars]
Vulnerability Patterns
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • 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 (6)

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

Critical
Category
Data Flow
Content
for attempt in range(self.max_retries + 1):
            req = urllib.request.Request(url, data=data_bytes, headers=headers, method=method)
            try:
                with urllib.request.urlopen(req, timeout=self.timeout) as response:
                    status = response.getcode()
                    response_headers = dict(response.info().items())
                    raw = response.read()
Confidence
98% confidence
Finding
The HTTP client will send the Bearer access token to any full URL because build_url accepts absolute http/https paths unchanged, and the generic request command exposes that path directly to the caller. In an agent skill meant for Meta Ads control, this enables authenticated token exfiltration and SSRF-style outbound requests to arbitrary hosts, well beyond the declared Meta Marketing API scope.

Context-Inappropriate Capability

High
Confidence
99% confidence
Finding
The client accepts arbitrary external URLs and unconditionally includes the Meta access token in the Authorization header for every request. That makes the skill substantially more dangerous in context, because a Meta Ads helper should never act as a general authenticated HTTP client capable of sending secrets to attacker-controlled infrastructure.

Credential Access

High
Category
Privilege Escalation
Content
def add_common(parser: argparse.ArgumentParser) -> None:
    parser.add_argument("--access-token", help="Meta access token. Defaults to META_ACCESS_TOKEN.")
    parser.add_argument("--account-id", help="Meta ad account ID. Defaults to META_AD_ACCOUNT_ID.")
    parser.add_argument("--api-version", default=DEFAULT_API_VERSION, help=f"Graph API version. Default: {DEFAULT_API_VERSION}")
    parser.add_argument("--graph-base", default=DEFAULT_GRAPH_BASE, help=f"Graph API base URL. Default: {DEFAULT_GRAPH_BASE}")
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The skill explicitly relies on environment variables, local file reads and writes, and outbound network access to the Meta Graph API, but it does not declare any tool scope such as permissions or allowed-tools. That creates an authorization gap: an agent may invoke broader capabilities than a reviewer or runtime expects, increasing the risk of unintended token exposure, unauthorized file access, or live ad-account mutations.

Tainted flow: 'path' from os.getenv (line 877, credential/environment) → pathlib.Path.write_text (file write)

Medium
Category
Data Flow
Content
return data
    path = pathlib.Path(output_path)
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
    result = dict(data)
    result["output_file"] = str(path)
    return result
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
The low-level request command bypasses the higher-level guardrails and allows arbitrary API paths, including endpoints outside ad-management operations described by the skill. In agent contexts, this broadens capability unexpectedly and can be abused to read or mutate unrelated Graph resources if the supplied token has wider permissions.

Static analysis

No suspicious patterns detected.