Back to skill

Security audit

openqbook

Security checks for vulnerabilities and agentic risk

Overview

The skill does what it claims, but its install flow and API/file handling create review-worthy security risks.

Review before installing. Use only with non-sensitive questions, avoid including secrets or proprietary details, pin or verify the skill file instead of using the mutable curl install command, keep OPENQBOOK_API_BASE pointed only at a trusted HTTPS endpoint, and periodically delete local polling state if it may contain sensitive content.

Vulnerability Patterns
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (3)

T03 · Remote Payload Retrieval and Execution

Error
Location
skill.md:32
Finding

Mutable Remote Skill Installed Without Integrity Verification

Content
View full analysis

Vulnerability Details

File Location: skill.md:32-55
Vulnerability Type: Remote payload retrieval through an unpinned, mutable URL
Risk Level: High

Vulnerable Code

bash
set -e

SKILL_NAME="openqbook-agent"
SKILL_URL="https://www.openqbook.com/skill.md"

if [ -n "${OPENQBOOK_SKILL_DIR:-}" ]; then
  BASE_DIR="$OPENQBOOK_SKILL_DIR"
elif [ -n "${AGENT_SKILLS_DIR:-}" ]; then
  BASE_DIR="$AGENT_SKILLS_DIR"
elif [ -n "${XDG_DATA_HOME:-}" ]; then
  BASE_DIR="$XDG_DATA_HOME/agent-skills"
elif [ -d ".agent-skills" ]; then
  BASE_DIR=".agent-skills"
elif [ -d ".skills" ]; then
  BASE_DIR=".skills"
else
  BASE_DIR="$HOME/.agent-skills"
fi

TARGET_DIR="$BASE_DIR/$SKILL_NAME"
mkdir -p "$TARGET_DIR"

curl -fsSL "$SKILL_URL" -o "$TARGET_DIR/SKILL.md"

Technical Analysis

The installation procedure downloads a Skill definition from a mutable remote URL and writes it directly into the Agent's active skill directory. It does not pin a release version, validate a cryptographic signature, or compare the downloaded file against an expected digest.

Although HTTPS protects the connection in transit under normal conditions, it does not ensure that the content currently hosted at the URL is identical to the version that was audited. The effective payload can change after review. Because the instructions tell the user to reload the Agent runtime's skill registry, a subsequently modified file can become trusted Agent instructions or executable Skill code.

Attack Path

  1. An attacker compromises the OpenQBook website, DNS path, TLS infrastructure, publishing account, or content deployment system.
  2. The attacker replaces https://www.openqbook.com/skill.md with a malicious Skill definition.
  3. A user executes the documented installation procedure.
  4. curl writes the attacker-controlled content directly to the runtime's Skill directory as SKILL.md.
  5. The user reloads the Agent run ...[truncated 597 chars]
Remediation
View remediation

Remediation Suggestions

  • Distribute the audited Skill inside a versioned package rather than retrieving the current file from a mutable URL.
  • Pin downloads to an immutable, version-specific release artifact.
  • Publish a SHA-256 digest through a separately protected channel and verify it before installation.
  • Prefer a cryptographic signature tied to a documented publisher key.
  • Download into a temporary file, validate it, set restrictive permissions, and atomically move it into place only after successful verification.
  • Refuse installation if signature or digest verification fails.
  • Avoid automatically replacing an existing installed Skill without explicit user confirmation and a reviewable change summary.

T09 · Insecure Skill Coding Practices

Warning
Location
skill.md:263
Finding

Bearer API Key Can Be Transmitted to an Arbitrary Configured Endpoint

Content
View full analysis

Vulnerability Details

File Location: skill.md:263-306
Vulnerability Type: Unrestricted API endpoint configuration combined with automatic credential attachment
Risk Level: Medium

Vulnerable Code

python
API_BASE_URL = os.getenv("OPENQBOOK_API_BASE", "")
API_KEY = os.getenv("OPENQBOOK_API_KEY", "")

STATE_DIR = os.path.expanduser("~/.openqbook/polling")

def _get_headers() -> Dict[str, str]:
    if not API_KEY:
        raise ValueError("OPENQBOOK_API_KEY environment variable is not set.")
    return {
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json"
    }

def ask_human_question(
    title: str,
    content: str = "",
    force: bool = False
) -> Dict[str, Any]:
    url = f"{API_BASE_URL}/questions"
    payload = {"title": title, "content": content, "force": force}

    response = requests.post(
        url,
        json=payload,
        headers=_get_headers()
    )
    response.raise_for_status()

The documented configuration also permits direct endpoint replacement:

bash
export OPENQBOOK_API_BASE="https://api.openqbook.com/api/v1"

Technical Analysis

OPENQBOOK_API_BASE is accepted without validating its scheme, hostname, port, or origin. The _get_headers() function attaches the OpenQBook bearer key to every request, regardless of the destination represented by that variable.

Consequently, control over the process environment or deployment configuration is sufficient to redirect authenticated requests to another server. The same requests may include developer questions, answer feedback, and resolution details. Allowing a non-HTTPS endpoint would additionally expose credentials and content to network interception.

This issue does not independently grant an unauthenticated remote party control of the environment. It becomes exploitable when an attacker, compromised launcher, poisoned configuration, or uns ...[truncated 1239 chars]

Remediation
View remediation

Remediation Suggestions

  • Use the documented HTTPS API endpoint as a secure default.
  • Parse the configured URL and reject schemes other than https.
  • Maintain an explicit allowlist of trusted API hostnames and expected ports.
  • Bind each credential to its intended origin and refuse to attach the OpenQBook token to another origin.
  • If custom or self-hosted endpoints are required, use a separate credential configured specifically for that endpoint.
  • Reject URLs containing embedded user information, fragments, unexpected ports, or ambiguous hostname encodings.
  • Apply explicit connection and read timeouts to all HTTP requests.
  • Document the sensitivity of environment-based configuration and ensure launchers prevent untrusted environment injection.
  • Rotate the API key immediately if it may have been sent to an untrusted endpoint.

T09 · Insecure Skill Coding Practices

Warning
Location
skill.md:523
Finding

Unvalidated Question IDs Permit State-File Path Traversal

Content
View full analysis

Vulnerability Details

File Location: skill.md:523-539
Vulnerability Type: Path traversal and arbitrary JSON file overwrite
Risk Level: Medium

Vulnerable Code

python
def _load_state(question_id: str) -> Optional[Dict[str, Any]]:
    """Load state from file."""
    filepath = os.path.join(STATE_DIR, f"{question_id}.json")
    if os.path.exists(filepath):
        with open(filepath, "r") as f:
            return json.load(f)
    return None

def _save_state(question_id: str, state: Dict[str, Any]) -> None:
    """Save state to file."""
    _ensure_state_dir()
    state["updated_at"] = datetime.now().isoformat()
    filepath = os.path.join(STATE_DIR, f"{question_id}.json")
    with open(filepath, "w") as f:
        json.dump(state, f, indent=2)

Technical Analysis

The code directly interpolates question_id into a filesystem path without validating that it is a simple identifier. os.path.join() does not prevent traversal. Values containing ../ components can resolve outside STATE_DIR, while an absolute path can cause the preceding state directory to be discarded.

Question IDs can originate from API responses and are subsequently passed to state-management functions such as init_polling() and poll_and_save(). A malicious or compromised API endpoint could therefore supply a crafted identifier. _save_state() opens the resolved path with mode "w", which creates or truncates the target file.

The .json suffix limits convenient target selection but does not eliminate the issue: an attacker can target existing JSON files, create files in writable directories, or use traversal and platform-specific path semantics to escape the intended directory. _load_state() also permits unauthorized reading and JSON parsing of files outside the state directory when a crafted identifier resolves to such a file.

Attack Path

  1. An attacker compromises the API service or redirects ...[truncated 1051 chars]
Remediation
View remediation

Remediation Suggestions

  • Enforce a strict identifier format before any filesystem operation, for example ^[A-Za-z0-9_-]+$, with a reasonable maximum length.
  • Reject empty identifiers, absolute paths, path separators, . components, and .. components.
  • Resolve the candidate path with pathlib.Path.resolve() and verify that it remains beneath the resolved STATE_DIR.
  • Consider mapping server-provided IDs to locally generated filenames, such as a SHA-256 digest or UUID, instead of using remote values directly.
  • Create the state directory with mode 0700 and state files with mode 0600.
  • Write to a securely created temporary file in the same directory, flush and synchronize it, and then use an atomic replacement.
  • Avoid following symbolic links when opening state files where the operating system and Python APIs permit it.
  • Validate the expected JSON schema and impose file-size limits before loading state.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Rogue AgentSelf-Modification, Session Persistence
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (6)

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The skill is explicitly designed to send user-supplied question content to an external OpenQBook service and to persist related data locally, but the description does not clearly warn users that prompts, problem details, and received answers leave the local environment and are stored on disk. In an agent setting, blocked tasks may include credentials, source code, internal architecture, or other sensitive material, so lack of disclosure materially increases the risk of accidental data exfiltration.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
50% 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 · skill.md (reported line 25)May include surrounding context.

Optionally configure custom API endpoint:

bash
export OPENQBOOK_API_BASE="https://api.openqbook.com/api/v1"

Installation (Multi-Path Adaptive)

External Transmission

Medium
Category
Data Exfiltration
Confidence
98% confidence
Finding

This function posts arbitrary title and content data to an external API, which is the core behavior of the skill. In the context of autonomous agents, blocked-task context often contains sensitive internal data; sending it to a third party without built-in sanitization, consent gates, or scope restrictions creates a real exfiltration path.

Content

Scanner excerpt · skill.md (reported line 306)May include surrounding context.

md
url = f"{API_BASE_URL}/questions"
payload = {"title": title, "content": content, "force": force}

response = requests.post(url, json=payload, headers=_get_headers())
response.raise_for_status()

data = response.json()

External Transmission

Medium
Category
Data Exfiltration
Confidence
85% confidence
Finding

Feedback comments are also transmitted to the external service and may contain details about system behavior, attempted mitigations, or environment-specific failures. While less severe than initial question posting, this still extends the exfiltration surface and can leak operational or debugging details to a third party.

Content

Scanner excerpt · skill.md (reported line 368)May include surrounding context.

md
"comment": comment
}

response = requests.post(url, json=payload, headers=_get_headers())

if response.status_code == 409:
    return {"success": False, "message": "Feedback already submitted."}

External Transmission

Medium
Category
Data Exfiltration
Confidence
82% confidence
Finding

Closing a question with a resolution sends final remediation details to the external service. Resolution text may summarize internal fixes, configuration changes, or root causes, which can reveal sensitive architecture or defensive posture.

Content

Scanner excerpt · skill.md (reported line 392)May include surrounding context.

md
url = f"{API_BASE_URL}/questions/{question_id}/close"
payload = {"resolution": resolution}

response = requests.post(url, json=payload, headers=_get_headers())

if response.status_code == 409:
    return {"success": False, "message": "Question already closed."}

Session Persistence

Medium
Category
Rogue Agent
Confidence
93% confidence
Finding

The skill saves polling state and answer content to predictable JSON files under ~/.openqbook/polling without any retention, access-control hardening, or sensitivity warning. Those files may contain externally provided answers and user-submitted problem context, creating local persistence of potentially sensitive data that other local users, backups, or forensic tools could access.

Content

Scanner excerpt · skill.md (reported line 534)May include surrounding context.

md
return None

def \_save\_state(question\_id: str, state: Dict\[str, Any]) -> None:
"""Save state to file."""
\_ensure\_state\_dir()
state\["updated\_at"] = datetime.now().isoformat()
filepath = os.path.join(STATE\_DIR, f"{question\_id}.json")

Static analysis

No suspicious patterns detected.