Back to skill

Security audit

Brave Rotator

Security checks for vulnerabilities and agentic risk

Overview

This Brave Search skill does what it claims, but it handles API keys unsafely by persisting full keys locally and logging a key prefix.

Install only if you are comfortable with Brave search queries leaving your environment and with this version managing API-key rotation locally. Before use, treat the state file as sensitive, restrict its permissions, avoid shared or unsafe BRAVE_KEY_STATE_FILE paths, and consider changing the implementation so it stores non-secret key labels or hashes instead of full API keys.

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
scripts/brave_search.py:38
Finding
Plaintext Persistence and Partial Logging of Brave API Credentials## Vulnerability Details **File Location**: `scripts/brave_search.py:38`, `scripts/brave_search.py:60`, `scripts/brave_search.py:100-113`, and `scripts/brave_search.py:171` **Vulnerability Type**: Plaintext sensitive-data storage and credential disclosure through logs **Risk Level**: Medium ### Vulnerable Code ```python def save_state(state): STATE_FILE.write_text(json.dumps(state, indent=2)) ``` ```python key_state = state["keys"].get(key, {}) ``` ```python state["keys"].setdefault(key, {})["last_success"] = time.time() state["keys"][key]["requests"] = state["keys"][key].get("requests", 0) + 1 save_state(state) ``` ```python if e.code in (429, 403): state["keys"].setdefault(key, {})["blocked_until"] = time.time() + 60 save_state(state) print(f"[brave-rotator] Key #{idx} rate limited, rotating...", file=sys.stderr) key, idx = pick_key(keys, state) ``` ```python print(f"[brave-rotator] Used key #{used_idx} ({used_key[:8]}...)", file=sys.stderr) ``` ### Technical Analysis The state structure uses the complete Brave API key as a dictionary key under `state["keys"]`. When `save_state()` serializes the structure, full credentials are written in plaintext to `~/.brave_key_state.json` or to the path specified through `BRAVE_KEY_STATE_FILE`. The file is created with `Path.write_text()` without explicitly enforcing restrictive permissions, validating file ownership, rejecting symbolic links, or performing an atomic secure write. Consequently, credential confidentiality depends on the process umask, existing file permissions, and the safety of the configured path. The state file may also retain credentials after they have been removed from `BRAVE_API_KEYS`, because stale entries are not purged. The documentation compounds the issue by describing the file as containing request counts and timestamps without disclosing that complete API keys are used as JSON property names. In addit ...[truncated 2154 chars]
Remediation
## Remediation Suggestions 1. **Do not use complete credentials as state identifiers.** Derive a non-reversible identifier using HMAC-SHA-256 with a locally protected random secret. A plain unkeyed short hash is less desirable because it may permit correlation or guessing when key formats have limited entropy. 2. **Remove credential prefixes from logs.** Log only the numeric key index or an independently generated non-secret label. 3. **Enforce restrictive state-file permissions.** Create the file with mode `0600`, verify that it is owned by the expected user, and reject files with unsafe ownership or permissions. 4. **Use secure atomic writes.** Write to a securely created temporary file in the same directory, apply restrictive permissions, flush it, and atomically replace the state file. 5. **Defend against symbolic-link attacks.** Reject symlinks and validate the configured state path before reading or writing it. 6. **Remove stale state records.** Retain state only for identifiers corresponding to the currently configured keys. 7. **Migrate existing installations.** Delete or securely replace state files that contain plaintext credentials and rotate any keys that may have been exposed. 8. **Correct the documentation.** Clearly describe the state data, required file protections, retention behavior, and the security implications of selecting a custom `BRAVE_KEY_STATE_FILE`.
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
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (11)

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

Critical
Category
Data Flow
Content
})
    
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            raw = resp.read()
            if resp.headers.get("Content-Encoding") == "gzip":
                raw = gzip.decompress(raw)
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

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

Critical
Category
Data Flow
Content
})
    
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            raw = resp.read()
            if resp.headers.get("Content-Encoding") == "gzip":
                raw = gzip.decompress(raw)
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill advertises and instructs use of capabilities that require environment access, filesystem reads/writes, and outbound network access, but it does not declare any explicit tool scope or permission boundary. That mismatch is dangerous because an agent or reviewer cannot easily constrain what the skill is allowed to do, increasing the risk of unintended secret access, persistent local state writes, and arbitrary network use.

Vague Triggers

Medium
Confidence
95% confidence
Finding
The trigger language is very broad, covering general web, news, image, and effectively any task requiring Brave Search, which can cause the skill to activate for many unrelated user requests. Overbroad activation increases attack surface by routing more interactions through a skill that uses network, environment secrets, and persistent local state, making accidental or unnecessary invocation more likely.

External Transmission

Medium
Category
Data Exfiltration
Content
STATE_FILE = Path(os.environ.get("BRAVE_KEY_STATE_FILE", Path.home() / ".brave_key_state.json"))
BRAVE_API_BASE = "https://api.search.brave.com/res/v1"


def load_state():
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
STATE_FILE = Path(os.environ.get("BRAVE_KEY_STATE_FILE", Path.home() / ".brave_key_state.json"))
BRAVE_API_BASE = "https://api.search.brave.com/res/v1"


def load_state():
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Tainted flow: 'STATE_FILE' from os.environ.get (line 26, credential/environment) → pathlib.Path.write_text (file write)

Medium
Category
Data Flow
Content
def save_state(state):
    STATE_FILE.write_text(json.dumps(state, indent=2))


def get_keys():
Confidence
88% confidence
Finding
The state file path is taken from the BRAVE_KEY_STATE_FILE environment variable and written without validation or permission hardening. In an agent or shared execution environment, a caller who can influence environment variables could redirect writes to an unintended file, causing file clobbering or disclosure of API-key usage metadata.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
User search queries are transmitted to a third-party service without any runtime disclosure or warning. In an agent context, queries may contain sensitive prompts, internal data, or user-identifying information, so silent transmission increases privacy and data-handling risk.

Natural-Language Policy Violations

Low
Confidence
88% confidence
Finding
The reference table sets `search_lang` to a default of `en`, which imposes a specific language setting in the documented behavior. The file does not offer user opt-in, alternatives, or a justification for enforcing English as the default locale.

Missing User Warnings

Low
Confidence
86% confidence
Finding
The script persists per-key usage state locally without notice, which may reveal operational metadata such as which keys exist, request timing, and usage counts. While not severe on its own, undisclosed persistence is a privacy and operational security concern, especially on multi-user systems.

Natural-Language Policy Violations

Low
Confidence
91% confidence
Finding
The function signature hard-codes `country="us"` and `lang="en"`, and the CLI mirrors those defaults, which biases behavior toward a specific locale and language. The file does not present this as an explicit opt-in choice or justify the locale restriction as region-specific.

Static analysis

No suspicious patterns detected.