Back to skill

Security audit

IONSEC Threat Intel

Security checks for vulnerabilities and agentic risk

Overview

This threat-intelligence skill is purpose-aligned, but it needs review because it can disclose submitted indicators and API keys in ways users may not expect.

Review before installing in any environment where IOCs, internal URLs, customer data, or API keys are sensitive. Avoid using --services all on confidential investigations, do not submit token-bearing or internal URLs to URLScan, prefer environment variables or a secret store over setup, restrict file permissions if config.json is used, and rotate any Shodan key used after cache files were created.

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 (4)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/setup.py:29
Finding
API Keys Are Echoed During Entry and Stored Without Restrictive File Permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/setup.py:29-31`, `scripts/setup.py:36-48`, and `scripts/setup.py:107-116` **Vulnerability Type**: Plaintext credential exposure and insecure local secret storage **Risk Level**: High ### Vulnerable Code ```python def save_config(config: dict): """Save config to file.""" config_path = get_config_path() with open(config_path, "w") as f: json.dump(config, f, indent=2) print(f"\n✅ Config saved to: {config_path}") ``` ```python def prompt_api_key(name: str, description: str, existing: str = "") -> str: """Prompt user for an API key.""" print(f"\n{name}") print("-" * len(name)) print(description) if existing: print(f"Current: {'*' * 8}{existing[-4:] if len(existing) > 4 else ''}") print("Press Enter to keep existing, or type new key:") else: print("Enter API key (leave empty to skip):") value = input("> ").strip() return value if value else existing ``` ```python for svc in services: existing = config.get(svc["key"], "") value = prompt_api_key( svc["name"], svc["desc"], existing ) if value: config[svc["key"]] = value # Save configuration ``` ### Technical Analysis The setup wizard reads API keys through Python's regular `input()` function. Terminal echo remains enabled, so each key is displayed while the user types it. This exposes credentials to screen recording, terminal capture, nearby observers, and some terminal logging mechanisms. The resulting keys are written in plaintext to the Skill-local `config.json` using the process's default file creation mode. The code does not: - Explicitly create the file with mode `0600`. - verify or correct the permissions of an existing file; - use an operating-system credential store; - perform an atomic, securely permissioned replacement; - warn users that the file contains reusable plaintext credentials. The actual expos ...[truncated 1402 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Replace `input()` with `getpass.getpass()` so secrets are not echoed: ```python from getpass import getpass value = getpass("> ").strip() ``` 2. Prefer environment variables or an operating-system keyring instead of project-local plaintext storage. 3. If file storage remains supported, create the file atomically with owner-only permissions: ```python import os import tempfile config_path.parent.mkdir(mode=0o700, parents=True, exist_ok=True) fd, temporary_path = tempfile.mkstemp(dir=config_path.parent) try: os.fchmod(fd, 0o600) with os.fdopen(fd, "w") as stream: json.dump(config, stream, indent=2) stream.flush() os.fsync(stream.fileno()) os.replace(temporary_path, config_path) os.chmod(config_path, 0o600) except Exception: try: os.unlink(temporary_path) except OSError: pass raise ``` 4. Check existing file permissions before reading or updating the configuration and reject group- or world-readable files. 5. Add `config.json` and `.cache/` to source-control ignore rules. 6. Clearly document that saved keys are reusable secrets and provide instructions for rotation after suspected exposure. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/services/shodan.py:41
Finding
Shodan API Key Is Written into Cache Filenames<![CDATA[ ## Vulnerability Details **File Location**: `scripts/services/shodan.py:41-45` and `scripts/services/base.py:51-58, 135-138, 175-177` **Vulnerability Type**: Credential exposure through sensitive cache keys **Risk Level**: High ### Vulnerable Code ```python def _make_request(self, url: str) -> Dict: """Make authenticated request to Shodan.""" # Shodan uses ?key= query param full_url = f"{url}?key={self.api_key}" if "?" not in url else f"{url}&key={self.api_key}" return super()._make_request(full_url) ``` The shared cache implementation derives a filename directly from the authenticated URL: ```python def _get_cache_key(self, observable: str, obs_type: str) -> str: """Generate cache key for a query.""" return f"{self.NAME}:{obs_type}:{observable.lower()}" def _get_cache_path(self, cache_key: str) -> Path: """Get filesystem path for cache key.""" safe_key = cache_key.replace('/', '_').replace(':', '_') return self._cache_dir / f"{safe_key}.json" ``` ```python # Check cache for GET requests if method == "GET": cache_key = f"{self.NAME}:url:{url}" cached = self._get_cached(url, "url") if cached: return cached ``` ```python # Cache successful GET responses if method == "GET": self._cache_response(url, "url", result) ``` ### Technical Analysis Shodan authentication is appended to the request URL as `?key=<API_KEY>`. The complete URL is then passed to the generic request and cache implementation. For GET requests, the generic cache uses that complete URL as the observable from which the cache key and filesystem path are generated. The filename sanitization only replaces `/` and `:` characters. It does not remove query parameters or credentials, and therefore preserves `?key=<API_KEY>` in the generated filename. Consequently, executing a Shodan query can create a file under `.cache` whose filename itself contains the Shodan API key. Filenames are commonly exposed in directory listings, back ...[truncated 1385 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Never derive cache filenames from raw authenticated URLs. 2. Remove credentials and other sensitive query parameters before cache-key generation. 3. Generate a fixed-length cache identifier using a cryptographic digest: ```python def _get_cache_path(self, cache_key: str) -> Path: digest = hashlib.sha256(cache_key.encode("utf-8")).hexdigest() return self._cache_dir / f"{self.NAME}_{digest}.json" ``` 4. Construct the cache identity from the service name, observable type, and observable only—not from headers, tokens, or authenticated URLs. 5. Where supported by the service, use an authorization header rather than a query-string credential. 6. Create the cache directory with mode `0700` and cache files with mode `0600`. 7. Delete existing cache files that may contain keys in their names and rotate any Shodan key that has already been used by the affected implementation. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/services/urlscan.py:66
Finding
Unseen URLs Are Automatically Submitted as Public URLScan Scans<![CDATA[ ## Vulnerability Details **File Location**: `scripts/services/urlscan.py:35-47` and `scripts/services/urlscan.py:66-78` **Vulnerability Type**: Undisclosed public disclosure and third-party retrieval of user-supplied URLs **Risk Level**: High ### Vulnerable Code ```python try: # First check if already scanned data = self._search_url(observable) if data.get("results"): return self._success_response(data) # If not, need to scan (requires API key for private scans) if self.api_key: data = self._submit_scan(observable) return self._success_response(data) return self._error_response("URL not previously scanned. Submit scan requires API key.") except Exception as e: return self._error_response(str(e)) ``` ```python def _submit_scan(self, url: str) -> Dict: """Submit URL for scanning (requires API key).""" import json payload = json.dumps({ "url": url, "public": "on" # Public scan }).encode() submit_url = f"{self.BASE_URL}/scan/" result = self._make_request(submit_url, method="POST", data=payload) return result ``` ### Technical Analysis When a URL does not already have URLScan results and an API key is configured, the adapter automatically submits it for a new scan. The request hardcodes `"public": "on"`, meaning the supplied URL and resulting scan are intentionally made public. There is no confirmation prompt, private-by-default behavior, or sanitization of sensitive URL components. The Skill documentation describes live URL scanning but does not disclose that an unscanned URL can be publicly published. URLs frequently contain sensitive information, including: - password-reset or invitation tokens; - signed object-storage URLs; - session identifiers; - internal hostnames and paths; - customer or incident identifiers; - authentication material embedded in query strings; - staging or non-public service addresses. Submission also causes URLS ...[truncated 1514 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Make private scanning the default wherever the URLScan account supports it. 2. Require explicit, informed user confirmation before any public submission. 3. Separate passive search from active submission through an explicit option such as `--submit` and require an additional `--public` flag for public scans. 4. Clearly state that URLScan will remotely retrieve the target and describe the selected visibility. 5. Parse URLs before submission and reject embedded usernames or passwords. 6. Warn about or redact sensitive query parameters such as `token`, `key`, `signature`, `session`, and `auth`. 7. Restrict accepted schemes to `http` and `https`. 8. Consider blocking internal, loopback, link-local, and private-network destinations unless the user explicitly overrides the protection. 9. Return a non-submitting result when no existing scan is found unless active submission was specifically requested. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/threat_intel.py:149
Finding
Google DNS and Cloudflare DNS Selections Are Silently Routed to DNS0<![CDATA[ ## Vulnerability Details **File Location**: `scripts/threat_intel.py:47-50`, `scripts/threat_intel.py:149-174`, and `scripts/services/dns.py:39-43, 56-67` **Vulnerability Type**: Incorrect service routing and unintended third-party data disclosure **Risk Level**: Medium ### Vulnerable Code All three service names are registered to the same class: ```python "dns0": DNSResolver, "google_dns": DNSResolver, "cloudflare_dns": DNSResolver, ``` The dispatcher does not pass the selected DNS service to the resolver constructor: ```python def get_service_instance(service_name: str, config: Dict): """Get initialized service instance.""" service_class = SERVICE_REGISTRY.get(service_name) if not service_class: raise ValueError(f"Unknown service: {service_name}") # Initialize with relevant config service_config = {} if service_name == "virustotal": service_config["api_key"] = config.get("vt_api_key") elif service_name == "greynoise": service_config["api_key"] = config.get("greynoise_api_key") elif service_name == "shodan": service_config["api_key"] = config.get("shodan_api_key") elif service_name == "otx": service_config["api_key"] = config.get("otx_api_key") elif service_name == "abuseipdb": service_config["api_key"] = config.get("abuseipdb_api_key") elif service_name == "urlscan": service_config["api_key"] = config.get("urlscan_api_key") elif service_name == "spur": service_config["api_key"] = config.get("spur_api_key") elif service_name == "validin": service_config["api_key"] = config.get("validin_api_key") return service_class(**service_config) ``` The DNS class defaults to DNS0: ```python def __init__(self, resolver: str = "dns0", **kwargs): super().__init__(**kwargs) self.resolver = resolver if self.resolver not in self.RESOLVERS: raise ValueError(f"Unknown resolver: {self.resolver}. Options: {list ...[truncated 2355 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Pass the selected resolver name explicitly: ```python def get_service_instance(service_name: str, config: Dict): service_class = SERVICE_REGISTRY.get(service_name) if not service_class: raise ValueError(f"Unknown service: {service_name}") if service_name in {"dns0", "google_dns", "cloudflare_dns"}: return DNSResolver(resolver=service_name) service_config = {} # Populate API-key configuration as appropriate. return service_class(**service_config) ``` Additional hardening should include: 1. Add unit tests that mock network access and assert the destination hostname for every registered DNS service. 2. Include the effective resolver name in each response so provider attribution can be verified. 3. Avoid registering several service names to a stateful implementation unless all required constructor options are explicitly supplied. 4. Ensure `--services all` performs one request per genuinely distinct provider. 5. Document the external provider that receives each observable and provide users with provider-level opt-out controls. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (32)

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
Claiming support for URLs, hashes, malware detection, and multiple vendors when the implementation only queries a single provider like Validin creates a deceptive security boundary. Users may submit sensitive observables or rely on nonexistent detections, resulting in blind spots during incident response or threat hunting.

Tp4

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding
Claiming support for URLs, hashes, malware detection, and multiple vendors when the implementation only queries a single provider like Validin creates a deceptive security boundary. Users may submit sensitive observables or rely on nonexistent detections, resulting in blind spots during incident response or threat hunting.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
Claiming support for URLs, hashes, malware detection, and multiple vendors when the implementation only queries a single provider like Validin creates a deceptive security boundary. Users may submit sensitive observables or rely on nonexistent detections, resulting in blind spots during incident response or threat hunting.

os.system() or os exec-family call

High
Category
Dangerous Code Execution
Content
print("Setup script not found.", file=sys.stderr)
        return False

    os.execv(sys.executable, [sys.executable, str(setup_script)])
    return True
Confidence
85% confidence
Finding
os.system() and os exec-family calls run shell commands with the process's full privileges, enabling arbitrary command execution.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The README promotes enrichment via 14+ external services and explicitly includes live URL scanning, but it does not clearly warn users that submitted IPs, domains, URLs, and hashes may be disclosed to third parties. In an incident-response context, observables can be sensitive or customer-confidential, so sending them to external providers without prominent notice can create privacy, confidentiality, and operational-security risk.

Lp3

Medium
Category
MCP Least Privilege
Confidence
88% confidence
Finding
The skill documents capabilities that involve environment variables, shell execution, file writing, and network access, but it does not declare any tool scope or permissions boundaries. In an agent setting, this increases the chance of unintended privileged execution and makes it harder for users or orchestrators to understand what the skill is allowed to do.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The skill encourages users to submit IPs, domains, URLs, and hashes to external threat-intelligence providers without warning that those observables may be disclosed to third parties. This can leak sensitive internal indicators, investigation targets, or customer data to outside services, which is especially risky in enterprise or incident-response contexts.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The tutorial instructs users to submit IPs, domains, URLs, and hashes to external services such as VirusTotal, Shodan, AbuseIPDB, OTX, and others, but it does not warn that these queries disclose potentially sensitive indicators and investigation context to third parties. In incident response and threat hunting workflows, premature IOC submission can tip off adversaries, leak client or internal telemetry, or violate privacy and data-handling requirements.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The documentation encourages querying multiple third-party threat intelligence services with user-provided indicators but does not warn that submitted IPs, domains, URLs, and hashes may be disclosed to external providers. In threat-intel workflows, indicators are often sensitive investigation artifacts, and sending them to commercial or community services can leak customer data, incident details, or detection targets to outside parties.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The script performs bulk enrichment by sending user-supplied observables to third-party threat intelligence services without any explicit consent prompt, privacy warning, or safeguard. In a threat-intel skill this behavior is functional, but it still creates a real data-exposure risk because internal IPs, domains, URLs, or hashes may be sensitive and will be transmitted externally in bulk.

Dynamic import via __import__()

Medium
Category
Dangerous Code Execution
Content
import sys
import argparse
sys.path.insert(0, str(__import__('pathlib').Path(__file__).parent))

from threat_intel import load_config, query_all_services
Confidence
75% confidence
Finding
Dynamic __import__() can load arbitrary modules at runtime, bypassing static analysis and potentially importing malicious code.

External Transmission

Medium
Category
Data Exfiltration
Content
RATE_LIMIT = 5  # Free tier: 5 requests/minute
    CACHE_TTL = 1800  # 30 minutes cache
    
    BASE_URL = "https://api.abuseipdb.com/api/v2/check"
    
    def __init__(self, api_key: Optional[str] = None, **kwargs):
        super().__init__(api_key=api_key, **kwargs)
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
87% confidence
Finding
This code performs outbound HTTP requests to threat-intelligence services and may send user-supplied observables plus API credentials in headers. While network access is part of the class purpose, there is no confirmation prompt, user-facing log, or explicit disclosure in this file that data will be transmitted to third-party services.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
This service sends queried domains to external public DNS providers (DNS0, Google DNS, Cloudflare) and a DNS0 reputation-check endpoint, which can disclose sensitive investigation targets to third parties. In a threat-intelligence skill, users may submit confidential IOCs related to active incidents, internal assets, or unreleased investigations, so the lack of any disclosure, consent mechanism, or privacy control creates a real data-exposure risk even though the network behavior is functionally intended.

External Transmission

Medium
Category
Data Exfiltration
Content
RATE_LIMIT = 1  # Free tier: 1 request/minute
    CACHE_TTL = 1800  # 30 minutes cache
    
    BASE_URL = "https://api.greynoise.io/v3/noise"
    
    def __init__(self, api_key: Optional[str] = None, **kwargs):
        super().__init__(api_key=api_key, **kwargs)
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
RATE_LIMIT = 1  # Free tier: 1 request/minute
    CACHE_TTL = 1800  # 30 minutes cache
    
    BASE_URL = "https://api.greynoise.io/v3/noise"
    
    def __init__(self, api_key: Optional[str] = None, **kwargs):
        super().__init__(api_key=api_key, **kwargs)
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
This code transmits the user-supplied observable to Pulsedive over the network, which can disclose sensitive investigation artifacts such as internal IPs, suspicious domains, URLs, or hashes to a third-party service. In a threat-intelligence skill, this behavior is expected functionality, but it still creates a real privacy and operational-security risk if users are not clearly warned before external lookup occurs.

External Transmission

Medium
Category
Data Exfiltration
Content
RATE_LIMIT = 60  # requests per minute
    CACHE_TTL = 3600  # 1 hour cache
    
    BASE_URL = "https://api.spur.us/v2"
    
    def __init__(self, api_key: Optional[str] = None, **kwargs):
        super().__init__(api_key=api_key, **kwargs)
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
This code submits user-provided URLs to urlscan.io, a third-party service, and explicitly sets scans as public. That can disclose sensitive investigation targets, internal URLs, tokens embedded in URLs, or customer data to an external party without any warning or consent at this layer, which is a real privacy and operational security risk in a threat-intelligence skill.

Intent-Code Divergence

Medium
Confidence
98% confidence
Finding
The class declares `SUPPORTED_TYPES = ["ip", "domain", "hash"]`, which signals that hash enrichment is implemented. However, `_query_hash` later returns an explicit error stating hash lookups are not supported, creating a direct contradiction between the code's declared capability and its actual behavior.

Intent-Code Divergence

Medium
Confidence
87% confidence
Finding
The presence of `_query_hash` as part of the normal query dispatch path suggests hash handling is part of the service contract, but the method's inline comments admit the service may not support hashes and it only returns an error. This creates intent-code divergence within the implementation: the interface advertises support while the method documents and enforces non-support.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
This client sends user-supplied observables such as IPs, domains, URLs, and file hashes to the external VirusTotal service, but the file contains no disclosure, consent, or sensitivity checks before transmission. In threat-intelligence workflows, observables can be confidential investigative artifacts, internal URLs, private domains, or customer data, so silent submission to a third party can leak sensitive information and alter exposure of an investigation.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The setup script collects sensitive API keys and persists them directly to config.json without warning the user that the file is plaintext or enforcing restrictive file permissions. In a threat-intel skill, these credentials may grant access to paid or rate-limited external services, so local disclosure can lead to account abuse, quota exhaustion, or exposure of investigation activity.

Unbounded Resource Access

Medium
Category
Excessive Agency
Content
{
            "key": "otx_api_key",
            "name": "AlienVault OTX",
            "desc": "Unlimited queries (registration required)\nGet key: https://otx.alienvault.com/settings",
            "required": False
        },
        {
Confidence
80% confidence
Finding
Skill allows unbounded resource consumption (API calls, storage, compute). Without rate limits or quotas, a compromised or misbehaving agent can cause denial-of-service or cost overruns.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The script sends observables such as IPs, domains, URLs, and hashes to multiple third-party threat intelligence providers without an explicit transmission warning or confirmation at query time. In security operations, observables may be sensitive or embargoed, so automatic disclosure to external services can leak incident details, internal infrastructure, or investigative targets.

Static analysis

No suspicious patterns detected.