Back to skill

Security audit

Review Responder

Security checks for vulnerabilities and agentic risk

Overview

The skill’s review-reply purpose is coherent, but it handles long-lived Google credentials and can publish public business replies with weak technical guardrails.

Review carefully before installing. Use this only if you are comfortable giving the skill local access to per-client Google OAuth credentials and authority to post Google Business Profile replies. Do not paste refresh tokens into chat; store them in a protected secrets location. Avoid exposing oauth_server.py publicly unless the client identifier handling, authentication, HTTPS, file permissions, and overwrite protections are fixed. Treat the reply command as privileged because it can post publicly without a built-in approval-state check.

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
oauth_server.py:63
Finding

Unauthenticated OAuth onboarding permits path traversal and client configuration overwrite

Content
View full analysis

Vulnerability Details

File Location: oauth_server.py:63-69, 86-90, 106-120
Vulnerability Type: Unauthenticated path traversal and arbitrary configuration overwrite
Risk Level: High

Vulnerable Code:

python
@app.route("/auth/start")
def auth_start():
    """Client visits this URL to begin authorization."""
    client_name = request.args.get("client", "").strip()
    if not client_name:
        return (
            "<h2>Missing client name</h2>"
            "<p>Add <code>?client=business-name</code> to the URL.</p>"
            "<p>Example: <code>/auth/start?client=joes-pizza</code></p>"
        ), 400

    session["client_name"] = client_name
python
@app.route(REDIRECT_PATH)
def auth_callback():
    """Google redirects here after the client authorizes."""
    client_name = session.get("client_name", "unknown")

    flow = get_flow()
    flow.fetch_token(authorization_response=request.url)

    creds = flow.credentials
    refresh_token = creds.refresh_token
python
    # Save the client config
    CLIENTS_DIR.mkdir(parents=True, exist_ok=True)
    config_path = CLIENTS_DIR / f"{client_name}.json"

    config = {
        "business_name": client_name,
        "account_id": "NEEDS_LOOKUP",
        "location_id": "NEEDS_LOOKUP",
        "oauth_client_id": GOOGLE_CLIENT_ID,
        "oauth_client_secret": GOOGLE_CLIENT_SECRET,
        "refresh_token": refresh_token,
        "authorized_at": datetime.now(timezone.utc).isoformat(),
        "notes": "",
    }

    with open(config_path, "w") as f:
        json.dump(config, f, indent=2)

Technical Analysis

The public /auth/start endpoint accepts the client query parameter without authentication or syntactic validation. This value is stored in the Flask session and later used directly to construct a filesystem path:

python
config_path = CLIENTS_DIR / f"{client_name}.json"

pathlib does not confine the resulting path to CLIENTS_DIR. A value containing traversa ...[truncated 2312 chars]

Remediation
View remediation

Remediation Suggestions

  1. Restrict client identifiers to a strict allowlist:

    python
    import re
    
    if not re.fullmatch(r"[A-Za-z0-9_-]{1,64}", client_name):
        return "Invalid client identifier", 400
    
  2. Resolve the destination path and verify containment before writing:

    python
    clients_root = CLIENTS_DIR.resolve()
    config_path = (clients_root / f"{client_name}.json").resolve()
    
    if config_path.parent != clients_root:
        return "Invalid destination", 400
    
  3. Require an authenticated operator to generate a random, single-use onboarding invitation bound to one predetermined client ID. Do not let the onboarding participant choose the destination identifier.

  4. Reject attempts to replace an existing configuration unless an authenticated operator explicitly initiates a rotation workflow.

  5. Create new files atomically with exclusive semantics, such as mode "x", and set restrictive permissions because the files contain OAuth secrets.

  6. Deploy the onboarding endpoint behind HTTPS and appropriate access controls rather than exposing the Flask development server directly.

T09 · Insecure Skill Coding Practices

Warning
Location
gbp_reviews.py:157
Finding

Documented operator approval gate is not enforced by the public-reply execution path

Content
View full analysis

Vulnerability Details

File Location: gbp_reviews.py:157-175, 215-235
Vulnerability Type: Missing authorization-state validation for a public posting operation
Risk Level: Medium

Vulnerable Code:

python
def post_reply(client_id: str, review_id: str, reply_text: str):
    """Post an approved reply to a specific review."""
    config = load_client_config(client_id)
    token = get_access_token(config)
    account_id = config["account_id"]
    location_id = config["location_id"]

    url = (
        f"https://mybusiness.googleapis.com/v4/"
        f"accounts/{account_id}/locations/{location_id}/"
        f"reviews/{review_id}/reply"
    )
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
    }
    body = {"comment": reply_text}

    resp = requests.put(url, headers=headers, json=body)
python
def main():
    parser = argparse.ArgumentParser(description="Google Business Profile Review Manager")
    sub = parser.add_subparsers(dest="command")

    # check command
    check_p = sub.add_parser("check", help="Poll for new unanswered reviews")
    check_p.add_argument("--client", required=True, help="Client ID")

    # reply command
    reply_p = sub.add_parser("reply", help="Post an approved reply")
    reply_p.add_argument("--client", required=True, help="Client ID")
    reply_p.add_argument("--review", required=True, help="Review ID")
    reply_p.add_argument("--reply", required=True, help="Reply text to post")

    # pending command
    sub.add_parser("pending", help="List all pending reviews")

    args = parser.parse_args()

    if args.command == "check":
        check_reviews(args.client)
    elif args.command == "reply":
        post_reply(args.client, args.review, args.reply)

Technical Analysis

The Skill documentation establishes an explicit authorization boundary: every public reply must receive operator approval before it is posted. However, the executable posting ...[truncated 2544 chars]

Remediation
View remediation

Remediation Suggestions

  1. Represent approval as a persistent, explicit state transition rather than relying on the caller's assertion.

  2. Before posting, require a pending record containing at least:

    • Client ID
    • Review ID
    • Exact approved reply text or its cryptographic hash
    • Approval channel and operator identity
    • Approval timestamp
    • Single-use approval nonce
  3. Make post_reply() atomically verify and consume that approval record. Reject direct calls when no valid approval exists.

  4. Bind approvals to the exact client, review, and reply content. Any edit after approval must invalidate the previous approval and require another confirmation.

  5. Use a short-lived signed approval token for asynchronous channels. Validate its signature, expiry, intended client, intended review, and content hash before posting.

  6. Reject review IDs that do not have a matching pending record generated by the polling workflow.

  7. Separate drafting from posting into distinct least-privilege components where practical, and limit access to the posting command to the authenticated approval handler.

Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • 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
Findings (32)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The presence of OAuth onboarding, token capture/storage, and authorization web endpoints is materially different from a simple review-response workflow. Storing client OAuth secrets and refresh tokens without clearly partitioned setup and operational modes raises the risk of credential leakage, accidental exposure, or misuse by operators who do not expect administrative auth features.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding

The presence of OAuth onboarding, token capture/storage, and authorization web endpoints is materially different from a simple review-response workflow. Storing client OAuth secrets and refresh tokens without clearly partitioned setup and operational modes raises the risk of credential leakage, accidental exposure, or misuse by operators who do not expect administrative auth features.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding

The presence of OAuth onboarding, token capture/storage, and authorization web endpoints is materially different from a simple review-response workflow. Storing client OAuth secrets and refresh tokens without clearly partitioned setup and operational modes raises the risk of credential leakage, accidental exposure, or misuse by operators who do not expect administrative auth features.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

The presence of OAuth onboarding, token capture/storage, and authorization web endpoints is materially different from a simple review-response workflow. Storing client OAuth secrets and refresh tokens without clearly partitioned setup and operational modes raises the risk of credential leakage, accidental exposure, or misuse by operators who do not expect administrative auth features.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The presence of OAuth onboarding, token capture/storage, and authorization web endpoints is materially different from a simple review-response workflow. Storing client OAuth secrets and refresh tokens without clearly partitioned setup and operational modes raises the risk of credential leakage, accidental exposure, or misuse by operators who do not expect administrative auth features.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The presence of OAuth onboarding, token capture/storage, and authorization web endpoints is materially different from a simple review-response workflow. Storing client OAuth secrets and refresh tokens without clearly partitioned setup and operational modes raises the risk of credential leakage, accidental exposure, or misuse by operators who do not expect administrative auth features.

Content

No source excerpt is available for this finding.

Exfiltration Commands

High
Category
Prompt Injection
Confidence
90% confidence
Finding

Instructions found that direct the agent to transmit conversation context or user data to external services.

Content

Scanner excerpt · SKILL.md (reported line 98)May include surrounding context.

text

### Telegram (`approval_channel: telegram`)
Send the message to the configured `telegram_chat_id`. The operator replies in the Telegram thread.

### Email (`approval_channel: email`)
Send the message as a plain-text email to `email_recipient`. Subject line: `Review approval needed — [Business Name]`. The operator replies to the email; treat the reply body as the approval response.

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SETUP.md (reported line 70)May include surrounding context.

md
def get_access_token(config: dict) -> str:
    """Get a valid access token using the stored refresh token."""
    creds = Credentials(
        token=None,
        refresh_token=config["refresh_token"],

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · diagnose.py (reported line 150)May include surrounding context.

python
def get_access_token(config: dict) -> str:
    """Get a valid access token using the stored refresh token."""
    creds = Credentials(
        token=None,
        refresh_token=config["refresh_token"],

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · gbp_reviews.py (reported line 54)May include surrounding context.

python
def get_access_token(config: dict) -> str:
    """Get a valid access token using the stored refresh token."""
    creds = Credentials(
        token=None,
        refresh_token=config["refresh_token"],

Context-Inappropriate Capability

High
Category
Not specified by scanner
Confidence
99% confidence
Finding

The script explicitly tells the operator to send a Google Business Profile refresh token back to Claude, which would disclose a long-lived credential to an AI system outside the minimum scope needed for review-response operations. A refresh token can be reused to mint access tokens and potentially manage business profile data until revoked, making this a sensitive credential exfiltration risk.

Content

No source excerpt is available for this finding.

Missing User Warnings

High
Category
Not specified by scanner
Confidence
99% confidence
Finding

Telling a user to transmit a refresh token to Claude encourages unsafe secret handling and bypasses standard credential-protection boundaries. In this skill context, the token grants access to Google Business Profile resources for client accounts, so disclosure could enable unauthorized reading or modification of business data.

Content

No source excerpt is available for this finding.

Ssd 3

High
Category
Not specified by scanner
Confidence
99% confidence
Finding

The plain-language instruction to return a refresh token to an AI assistant is a direct secret-disclosure pattern. Because this skill operates in a client-account review-response workflow, exposing a token would be especially dangerous: it could let anyone with access to the token authenticate to Google APIs and act on behalf of the managed business profile.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The 2.0.0 changelog entry still uses the phrase "HIPAA-safe," which can misrepresent the compliance posture of the skill and imply a certification or guarantee the workflow does not actually provide. In a medical review-response context, overstating HIPAA assurances is risky because operators may rely on that claim when handling sensitive patient-related communications, increasing legal, compliance, and reputational exposure.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
60% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · SETUP.md (reported line 10)May include surrounding context.

pip install google-auth google-auth-oauthlib requests

text

### 2. Create a Google Cloud Project
1. Go to https://console.cloud.google.com
2. Create a new project (e.g., "Review Responder")
3. Enable the **Google My Business API** (also called Business Profile API)

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
98% confidence
Finding

The setup instructs operators to store OAuth client secrets and long-lived refresh tokens directly in per-client JSON files, but provides no guidance on encryption, filesystem permissions, secret management, or exclusion from source control. If those files are copied, backed up insecurely, exposed through the workspace, or committed to a repository, an attacker could obtain persistent API access to clients' Google Business Profile accounts and act as those businesses.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding

The skill describes behavior that requires file access, network access, and local state management, but it does not declare any explicit tool scope or allowed-tools boundary. That omission weakens reviewability and enforcement, making it easier for an implementation to access credentials, local files, or external services beyond what operators expect.

Content

No source excerpt is available for this finding.

Dynamic import via __import__()

Medium
Category
Dangerous Code Execution
Confidence
75% confidence
Finding

Dynamic import() can load arbitrary modules at runtime, bypassing static analysis and potentially importing malicious code.

Content

Scanner excerpt · diagnose.py (reported line 57)May include surrounding context.

python
missing = []
    for mod in ("requests", "google.oauth2.credentials", "google.auth.transport.requests"):
        try:
            __import__(mod)
        except ImportError:
            missing.append(mod)
    if missing:

External Transmission

Medium
Category
Data Exfiltration
Confidence
80% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · diagnose.py (reported line 212)May include surrounding context.

python
report(FAIL, "test telegram", "need both botToken (openclaw.json) and telegram_chat_id (config) to send a test")
        return
    import requests
    r = requests.post(f"https://api.telegram.org/bot{token}/sendMessage",
                      json={"chat_id": chat_id, "text": "Review Responder diagnostic: Telegram path is working."}, timeout=20)
    if r.status_code == 200:
        report(OK, "test telegram", "message sent — check your phone. If it arrives, the notification path is fine.")

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · diagnose.py (reported line 212)May include surrounding context.

python
report(FAIL, "test telegram", "need both botToken (openclaw.json) and telegram_chat_id (config) to send a test")
        return
    import requests
    r = requests.post(f"https://api.telegram.org/bot{token}/sendMessage",
                      json={"chat_id": chat_id, "text": "Review Responder diagnostic: Telegram path is working."}, timeout=20)
    if r.status_code == 200:
        report(OK, "test telegram", "message sent — check your phone. If it arrives, the notification path is fine.")

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

The function persists reviewer name, rating, comment, and timestamps into JSON files under the pending directory. Although the code comments describe the purpose, there is no user-facing disclosure or confirmation that customer-generated content will be stored on disk, which is a privacy-relevant operation.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
80% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · gbp_reviews.py (reported line 175)May include surrounding context.

python
}
    body = {"comment": reply_text}

    resp = requests.put(url, headers=headers, json=body)
    if resp.status_code in (200, 201):
        print(f"Reply posted successfully to review {review_id}.")
        # Update log

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The script reassures the user that the client secret is not saved or printed, but later exposes an equally sensitive credential by printing the refresh token and instructing the user to share it. That mismatch can mislead operators into lowering their guard around a token that provides durable API access.

Content

No source excerpt is available for this finding.

Tainted flow: 'h' from input (line 55, user input) → requests.get (network output)

Medium
Category
Data Flow
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.

Content

Scanner excerpt · get_token_interactive.py (reported line 57)May include surrounding context.

python
print("\nLooking up your Business Profile account(s) and location(s)...\n")
    h = {"Authorization": f"Bearer {token}"}
    try:
        ar = requests.get(
            "https://mybusinessaccountmanagement.googleapis.com/v1/accounts",
            headers=h, timeout=30,
        )

Tainted flow: 'h' from input (line 55, user input) → requests.get (network output)

Medium
Category
Data Flow
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.

Content

Scanner excerpt · get_token_interactive.py (reported line 67)May include surrounding context.

python
for acct in accounts:
            name = acct.get("name", "")  # e.g. "accounts/123456789"
            print(f"ACCOUNT: {name}   ({acct.get('accountName','')})")
            lr = requests.get(
                f"https://mybusinessbusinessinformation.googleapis.com/v1/{name}/locations",
                headers=h,
                params={"readMask": "name,title,storefrontAddress"},

Static analysis

No suspicious patterns detected.