Back to skill

Security audit

Dialogflow CX to CX Agent Studio Migration Skill

Security checks for vulnerabilities and agentic risk

Overview

This migration skill mostly matches its stated purpose, but it needs review because it can send Google access tokens to unvalidated API URLs and makes durable cloud and local changes.

Install only if you understand that it will use your Google ADC identity, write exported Dialogflow agent contents to local disk, and create or modify CES resources. Use a non-production project or least-privilege service account, keep the default Google API endpoints, avoid custom base URLs unless you trust them completely, review the output directory for sensitive exported data, and prefer `--export-only` first when evaluating the migration.

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/migrate.py:36
Finding
ADC Bearer Token Disclosure Through Unrestricted API Base URLs<![CDATA[ ## Vulnerability Details **File Location**: `scripts/migrate.py:36-39`, `scripts/migrate.py:47-50`, `scripts/migrate.py:247-248`, `scripts/migrate.py:283-286` **Vulnerability Type**: Credential disclosure through attacker-controlled request destinations **Risk Level**: High ### Vulnerable Code ```python def http_request(method: str, url: str, token: str, **kwargs) -> Dict[str, Any]: headers = kwargs.pop("headers", {}) headers.setdefault("Authorization", f"Bearer {token}") headers.setdefault("Content-Type", "application/json") response = requests.request(method, url, headers=headers, **kwargs) ``` ```python def wait_operation(base_url: str, op_name: str, token: str, timeout_s: int = 900) -> Dict[str, Any]: start = time.time() delay = 2.0 if op_name.startswith("http"): op_url = op_name else: op_url = f"{base_url}/{op_name}" ``` ```python parser.add_argument("--dfcx-base-url", default=DEFAULT_DFCX_BASE) parser.add_argument("--ces-base-url", default=DEFAULT_CES_BASE) ``` ```python token = get_access_token(list(set(DFCX_SCOPES + CES_SCOPES))) dfcx_agent = get_dfcx_agent(args.dfcx_base_url, args.dfcx_agent, token) ``` ### Technical Analysis The script accepts the Dialogflow CX and CES API base URLs directly from command-line arguments without validating their schemes, hosts, ports, or origins. Every request made through `http_request` receives the Google ADC bearer token in its `Authorization` header, regardless of the request destination. The acquired token requests the union of the Cloud Platform, Dialogflow, and CES scopes. Consequently, a custom endpoint does not merely receive migration data; it receives a live Google OAuth bearer token whose effective authority is determined by both the requested scopes and the IAM permissions of the executing identity. The long-running-operation logic introduces an additional trust-boundary issue. If an operation name starts with `http`, it is treated as an abs ...[truncated 2048 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Allowlist the expected Google API origins: - `https://dialogflow.googleapis.com` - `https://ces.googleapis.com` 2. Parse URLs with `urllib.parse.urlsplit` and reject: - Non-HTTPS schemes. - Embedded user information. - Unexpected hosts or ports. - Fragments and malformed authorities. 3. Validate every absolute operation URL against the validated origin before sending credentials. 4. Prefer resolving operation resource names against a fixed, trusted API base rather than accepting absolute operation URLs. 5. If custom endpoints are required for testing, require an explicit unsafe-development flag and do not send production ADC credentials to them. 6. Acquire separate least-privilege credentials for Dialogflow and CES instead of requesting the union of all scopes in one token. 7. Ensure errors and logs never print authorization headers or tokens. 8. Consider configuring short-lived credentials and monitoring API audit logs for token misuse. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/migrate.py:115
Finding
Unbounded Extraction and Processing of Remotely Supplied ZIP Archives<![CDATA[ ## Vulnerability Details **File Location**: `scripts/migrate.py:115-157` **Vulnerability Type**: Unsafe archive processing and resource-exhaustion risk **Risk Level**: Medium ### Vulnerable Code ```python def summarize_export(zip_bytes: bytes, output_dir: str) -> Dict[str, Any]: summary: Dict[str, Any] = { "component_counts": {}, "components": {}, "unclassified_files": [], } component_index: Dict[str, List[Dict[str, Any]]] = {} if not zipfile.is_zipfile(io.BytesIO(zip_bytes)): summary["note"] = "Export content is not a zip archive. Skipping detailed parsing." return summary os.makedirs(output_dir, exist_ok=True) zf = zipfile.ZipFile(io.BytesIO(zip_bytes)) zf.extractall(output_dir) for path in zf.namelist(): if not path.endswith(".json"): continue if path.endswith("/agent.json") or path == "agent.json": data = parse_json_from_zip(zf, path) summary["agent"] = { "name": data.get("name"), "displayName": data.get("displayName"), "defaultLanguageCode": data.get("defaultLanguageCode"), "supportedLanguageCodes": data.get("supportedLanguageCodes", []), "timeZone": data.get("timeZone"), "startFlow": data.get("startFlow"), "startPlaybook": data.get("startPlaybook"), } continue category = classify_path(path) if category is None: summary["unclassified_files"].append(path) continue data = parse_json_from_zip(zf, path) entry = { "file": path, "name": data.get("name"), "displayName": data.get("displayName"), } component_index.setdefault(category, []).append(entry) ``` ### Technical Analysis The script checks only whether the supplied bytes have a valid ZIP structure before calling `extractall`. It ...[truncated 2532 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Inspect every `ZipInfo` entry before extraction. 2. Enforce conservative limits on: - Total member count. - Maximum uncompressed size per member. - Maximum total uncompressed size. - Maximum compression ratio. - Maximum JSON input size. 3. Reject absolute member paths, parent-directory components, symbolic links, device files, and other special entry types. 4. Resolve each destination with `os.path.realpath` or `pathlib.Path.resolve` and verify it remains inside the intended extraction directory. 5. Extract entries individually only after validation instead of calling `extractall`. 6. Check available disk capacity before extraction and use a dedicated directory with an appropriate storage quota. 7. Stream large content where practical rather than retaining the archive and parsed documents entirely in memory. 8. Stop processing and delete partial output if any archive member violates policy. 9. Process the archive in a context manager to guarantee closure: ```python with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: ... ``` 10. Combine these controls with strict API-origin validation so untrusted endpoints cannot supply export archives while receiving cloud credentials. ]]>
Vulnerability Patterns
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • 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
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (3)

Credential Access

High
Category
Privilege Escalation
Content
if not creds.valid:
        creds.refresh(Request())
    if not creds.token:
        raise RuntimeError("Failed to obtain access token.")
    return creds.token
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
93% confidence
Finding
The skill advertises and documents operations that read and write local files and make network calls to Google APIs, but it does not declare any explicit tool scope or permissions boundary in the skill metadata. This creates a transparency and containment problem: users and enforcement layers may not understand that the skill can export sensitive agent data to disk and create or modify remote CES resources.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The skill description explains what the migration does, but it does not clearly warn users that execution will create or modify CES resources and write exported Dialogflow CX agent data to local disk. This omission can lead to unintended data exposure, persistence of potentially sensitive configuration artifacts, and accidental changes in a production cloud environment.

Static analysis

No suspicious patterns detected.