Back to skill

Security audit

Klaus IOC Scanner

Security checks for vulnerabilities and agentic risk

Overview

This IOC scanner does what it claims, but it can send full user-provided URLs to VirusTotal automatically without a clear consent step or privacy warning.

Review before installing. Use this only for indicators you are comfortable sharing with VirusTotal and AbuseIPDB. Do not submit internal URLs, password-reset links, signed cloud-storage URLs, customer data, or URLs containing tokens unless you have approval. Prefer adding an explicit confirmation or lookup-only mode before use.

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

Warning
Location
src/ioc_scan.py:153
Finding
Unsanitized URLs and API credentials are transmitted to VirusTotal<![CDATA[ ## Vulnerability Details **File Location**: `src/ioc_scan.py:153-195` **Vulnerability Type**: Sensitive information disclosure through third-party API requests **Risk Level**: Medium ### Vulnerable Code ```python def check_url_virustotal(url: str) -> Optional[Dict]: """Consulta URL no VirusTotal (relatório existente).""" if not VIRUSTOTAL_API_KEY: return None url_encoded = urllib.parse.quote(url, safe='') url_api = f"{VT_API_URL_V2}/url/report" params = { 'apikey': VIRUSTOTAL_API_KEY, 'resource': url } try: response = requests.get(url_api, params=params, timeout=30) if response.status_code == 200: data = response.json() if data.get('response_code') == 1: return data return None except Exception as e: print(f"[VirusTotal] Erro: {e}") return None def scan_url_virustotal(url: str) -> Optional[Dict]: """Submete URL para scan no VirusTotal.""" if not VIRUSTOTAL_API_KEY: return None url_api = f"{VT_API_URL_V2}/url/scan" data = { 'apikey': VIRUSTOTAL_API_KEY, 'url': url } try: response = requests.post(url_api, data=data, timeout=30) if response.status_code == 200: result = response.json() # Aguarda e consulta novamente time.sleep(3) return check_url_virustotal(url) return None except Exception as e: print(f"[VirusTotal Scan] Erro: {e}") return None ``` The automatic submission behavior is invoked by the following logic in `scan_ioc`: ```python elif ioc_type == 'url': print(f"[*] Consultando URL: {ioc}") vt_data = check_url_virustotal(ioc) if not vt_data: print(f"[*] Nenhum resultado, submetendo para scan...") vt_data = scan_url_virustotal(ioc) ``` ### Technical Analysis The scanner sends the complete user-supplied URL to VirusTotal w ...[truncated 3575 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. **Require explicit consent before submission** - Separate passive report lookup from active URL submission. - Do not automatically call the scan endpoint when no report exists. - Prompt the user before submitting an unknown URL and clearly identify VirusTotal as the recipient. 2. **Sanitize URLs before transmission** - Reject URLs containing embedded user information. - Remove fragments before lookup or submission. - Remove or redact sensitive query parameters such as `token`, `key`, `apikey`, `access_token`, `session`, `auth`, `signature`, `sig`, and password-reset parameters. - Prefer extracting and checking only the hostname when full-path analysis is not necessary. - Warn users that sanitization may reduce URL-specific reputation accuracy. 3. **Protect the VirusTotal API key** - Migrate to a supported VirusTotal API version that accepts the API key in an authorization header, where available. - Avoid placing credentials in query strings. - Ensure HTTP client, proxy, and application logging does not record authorization headers, request bodies, or sensitive query strings. - Rotate the existing key if it may already have appeared in retained logs. 4. **Provide privacy-preserving operating modes** - Add a lookup-only mode that never submits unknown URLs. - Make lookup-only behavior the default. - Add a hostname-only mode for sensitive or internal URLs. - Clearly document what data is sent, to which provider, and under what conditions. 5. **Validate destination and input** - Parse URLs with `urllib.parse.urlsplit`. - Permit only expected schemes such as HTTP and HTTPS. - Reject malformed URLs and URLs containing credentials. - Consider blocking internal hostnames and private or loopback addresses unless the user explicitly overrides the restriction. 6. **Minimize retained sensitive data** - Avoid printing complete sensitive URLs in console output. - Redact ...[truncated 154 chars]
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (12)

Tainted flow: 'params' from os.environ.get (line 156, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
}
    
    try:
        response = requests.get(url, params=params, headers=headers, timeout=30)
        if response.status_code == 200:
            return response.json()
        else:
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'params' from os.environ.get (line 156, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
}
    
    try:
        response = requests.get(url, params=params, timeout=30)
        if response.status_code == 200:
            data = response.json()
            if data.get('response_code') == 1:
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'params' from os.environ.get (line 156, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
}
    
    try:
        response = requests.get(url, params=params, timeout=30)
        if response.status_code == 200:
            data = response.json()
            if data.get('response_code') == 1:
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'params' from os.environ.get (line 156, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
}
    
    try:
        response = requests.get(url_api, params=params, timeout=30)
        if response.status_code == 200:
            data = response.json()
            if data.get('response_code') == 1:
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Missing User Warnings

High
Confidence
98% confidence
Finding
The skill documentation does not warn users that submitted URLs, domains, and IPs will be transmitted to VirusTotal and AbuseIPDB. This is dangerous because IOCs may contain sensitive investigation data, internal infrastructure details, or unreleased phishing samples, and submitting them to external reputation services can leak confidential information and potentially make the artifacts visible to third parties.

Vague Triggers

Medium
Confidence
95% confidence
Finding
The trigger phrases are broad enough that ordinary user requests like 'verificar', 'scan', or 'é malicioso?' could invoke the skill unintentionally. Because this skill sends submitted indicators to external services, accidental invocation can cause unintended disclosure of user-provided URLs, domains, or IPs and create confusing or privacy-impacting behavior.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The manifest explicitly states that URLs, domains, and IPs will be scanned using VirusTotal and AbuseIPDB, which implies user-supplied indicators are transmitted to third-party services. Without a clear warning in the skill description or metadata, users may unknowingly disclose sensitive internal infrastructure indicators, creating privacy, confidentiality, and operational exposure risks.

External Transmission

Medium
Category
Data Exfiltration
Content
VIRUSTOTAL_API_KEY = os.environ.get("VIRUSTOTAL_API_KEY", "")
ABUSEIPDB_API_KEY = os.environ.get("ABUSEIPDB_API_KEY", "")
VT_API_URL_V2 = "https://www.virustotal.com/vtapi/v2"
ABUSEIPDB_URL = "https://api.abuseipdb.com/api/v2"


def extract_iocs(text: str) -> List[Tuple[str, str]]:
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
When no existing VirusTotal report is found, the script automatically submits the full user-provided URL to VirusTotal for scanning. That creates a real privacy and operational risk because sensitive internal, pre-release, customer, or incident-response URLs may be disclosed to a third party without an explicit warning or consent at the point of action.

Tainted flow: 'data' from requests.post (line 164, network input) → requests.post (network output)

Medium
Category
Data Flow
Content
}
    
    try:
        response = requests.post(url_api, data=data, timeout=30)
        if response.status_code == 200:
            result = response.json()
            # Aguarda e consulta novamente
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.

Natural-Language Policy Violations

Low
Confidence
79% confidence
Finding
The visible natural-language fields in the manifest are written in Portuguese, including the main description and command description, with no indication that language choice is configurable or that the skill is intentionally restricted to Portuguese-speaking users. Under the stated policy, forcing a specific language without opt-in can be a locale-policy issue.

Natural-Language Policy Violations

Low
Confidence
84% confidence
Finding
The usage/help text includes a hard-coded Chinese label ("用法:") while the rest of the script is primarily Portuguese and English, with no user option to choose language or locale. This imposes a specific locale presentation without opt-in and may violate language/locale policy requirements.

Static analysis

No suspicious patterns detected.