Back to skill

Security audit

Xanadu Stock Screener

Security checks for vulnerabilities and agentic risk

Overview

The stock screener mostly matches its stated purpose, but it bundles payment-charging code with a hardcoded billing key and weak file scoping that users should review before installing.

Review this skill carefully before installing. Its stock search behavior is straightforward, but the package includes payment-charging code and a hardcoded SkillPay key; the publisher should remove or rotate the key, document billing consent and data transmission, and validate watchlist names before this is used with real accounts or sensitive local files.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/billing_config.py:1
Finding

Hard-Coded SkillPay API Credential Transmitted with User Data

Content
View full analysis

Vulnerability Details

File Location: scripts/billing_config.py:1-4, scripts/billing.py:11-17, and scripts/billing.py:29-46
Vulnerability Type: Hard-coded secret and insecure credential transmission
Risk Level: High

Vulnerable Code

scripts/billing_config.py:1-4:

python
SKILLPAY_API_KEY = "sk_41b22ea52b5a803442973b84ec1cd8f395ed098a3890905f8a196a4ffdf43c87"
SKILL_ID = "xanadu-stock-screener"
OWNER_WALLET = "0xF194917738617118dfff40E0542cea20Cf7dDC55"
DEFAULT_PRICE = 0.001

scripts/billing.py:11-17:

python
# Load config
try:
    from billing_config import SKILLPAY_API_KEY, SKILL_ID, DEFAULT_PRICE, OWNER_WALLET
except ImportError:
    SKILLPAY_API_KEY = os.environ.get("SKILLPAY_API_KEY")
    SKILL_ID = "xanadu-social-manager"
    DEFAULT_PRICE = 0.001
    OWNER_WALLET = None

scripts/billing.py:29-46:

python
def charge(self, user_id: str, amount: float = None) -> Dict:
    """Charge user for skill usage"""
    amount = amount or DEFAULT_PRICE
    
    try:
        response = requests.post(
            f"{self.base_url}/charge",
            json={
                "api_key": self.api_key,
                "skill_id": self.skill_id,
                "user_id": user_id,
                "amount": amount
            },
            timeout=10
        )
        return response.json()
    except Exception as e:
        return {"success": False, "error": str(e)}

Technical Analysis

A live-looking SkillPay API key is embedded directly in a source file distributed with the project. Anyone who can read the package can recover and attempt to reuse this credential. The billing client subsequently places that credential, a caller-supplied user identifier, the skill identifier, and the requested charge amount in the body of an outbound request to https://api.skillpay.me/v1/charge.

HTTPS protects the connection in transit when cert ...[truncated 2195 chars]

Remediation
View remediation

Remediation Suggestions

  1. Immediately revoke and rotate the exposed SkillPay API key. Treat it as compromised even if the repository was not intentionally public.
  2. Remove the key from the current source and, where applicable, purge it from version-control history and distributed artifacts.
  3. Retrieve credentials from a deployment secret manager or environment variable. Do not provide a source-code fallback containing a real secret.
  4. Send credentials through the service's documented authorization mechanism, preferably an Authorization header, rather than including them in the request body.
  5. Configure application and proxy logging to redact authorization headers, API keys, user identifiers, and payment data.
  6. Apply server-side restrictions to the credential, including minimum scopes, spending limits, rate limits, short expiration, and source restrictions where supported.
  7. Validate charge amounts against an approved server-side price table. Do not allow an arbitrary caller-controlled amount to define billing behavior.
  8. Obtain explicit user consent before transmitting a user identifier and document the recipient, purpose, retention policy, and privacy implications.
  9. Remove billing.py and billing_config.py if billing is not required by the implemented stock-screening workflow. If billing is retained, reconcile the copied social-media identifiers and tiers with the declared stock-screener functionality.
  10. Add automated secret scanning and pre-commit or CI controls to prevent future credential commits.

T09 · Insecure Skill Coding Practices

Error
Location
scripts/screener.py:141
Finding

Watchlist Name Path Traversal Enables JSON File Read and Overwrite

Content
View full analysis

Vulnerability Details

File Location: scripts/screener.py:141-152, scripts/screener.py:175-186, and scripts/screener.py:216-217
Vulnerability Type: Path traversal and arbitrary file access
Risk Level: High

Vulnerable Code

scripts/screener.py:141-152:

python
def save_watchlist(name: str, stocks: list[dict]):
    """Save stocks to a watchlist file."""
    watchlist_file = DEFAULT_DATA_DIR / "watchlists" / f"{name}.json"
    watchlist_file.parent.mkdir(parents=True, exist_ok=True)
    
    with open(watchlist_file, "w") as f:
        json.dump({
            "name": name,
            "created": datetime.now().isoformat(),
            "stocks": stocks
        }, f, indent=2)
    
    print(f"Saved {len(stocks)} stocks to watchlist '{name}'")

scripts/screener.py:175-186:

python
def show_watchlist(name: str):
    """Show stocks in a watchlist."""
    watchlist_file = DEFAULT_DATA_DIR / "watchlists" / f"{name}.json"
    if not watchlist_file.exists():
        print(f"Watchlist '{name}' not found")
        return
    
    with open(watchlist_file) as f:
        data = json.load(f)
    
    print(f"\n# {data['name']}")
    print(f"Created: {data['created']}")
    print_results(data['stocks'])

scripts/screener.py:216-217:

python
show_parser = subparsers.add_parser("show-watchlist", help="Show stocks in watchlist")
show_parser.add_argument("name", help="Watchlist name")

Technical Analysis

The command-line-controlled watchlist name is appended directly to the watchlist directory without validation or a post-resolution containment check. pathlib.Path preserves traversal components such as .., so a name such as ../../target produces a path ending in watchlists/../../target.json, escaping the intended storage directory after filesystem resolution.

The write operation also calls `watchlist_file.parent.mkdir(parents=True, ...[truncated 2493 chars]

Remediation
View remediation

Remediation Suggestions

  1. Restrict watchlist names to a conservative allowlist, such as ^[A-Za-z0-9_-]{1,64}$.
  2. Explicitly reject /, \, absolute paths, . and .. path components, null characters, and platform-specific separators.
  3. Resolve both the watchlist root and candidate path and verify containment before any read, write, existence check, or directory creation. For example:
python
import re

WATCHLIST_DIR = (DEFAULT_DATA_DIR / "watchlists").resolve()

def watchlist_path(name: str) -> Path:
    if not re.fullmatch(r"[A-Za-z0-9_-]{1,64}", name):
        raise ValueError("Invalid watchlist name")

    candidate = (WATCHLIST_DIR / f"{name}.json").resolve()
    if candidate.parent != WATCHLIST_DIR:
        raise ValueError("Watchlist path escapes storage directory")
    return candidate
  1. Create only the fixed WATCHLIST_DIR; do not call mkdir() on a parent path derived from user input.
  2. Defend against symlink redirection. Use platform-appropriate no-follow behavior where available, verify resolved targets immediately before access, and keep the storage directory inaccessible to untrusted local users.
  3. Use atomic writes through a safely created temporary file in the fixed directory, apply restrictive permissions, flush data, and replace the final file atomically.
  4. Validate the JSON schema, file size, and data types before displaying a saved watchlist.
  5. Add tests covering absolute paths, nested paths, ../, mixed separators, repeated traversal, symlinks, and encoded or Unicode separator variants.

T08 · Insecure Dependencies

Warning
Location
scripts/screener.py:11
Finding

Unpinned Third-Party Dependency Creates a Mutable Supply-Chain Risk

Content
View full analysis

Vulnerability Details

File Location: scripts/screener.py:11-15 and SKILL.md:78-81
Vulnerability Type: Unpinned and unverifiable third-party dependency installation
Risk Level: Medium

Vulnerable Code

scripts/screener.py:11-15:

python
try:
    import yfinance as yf
except ImportError:
    print("Install yfinance: pip install yfinance")
    raise

SKILL.md:78-81:

markdown
## Requirements

- Python 3.10+
- `yfinance` package (Yahoo Finance data)

Technical Analysis

The project requires yfinance but supplies no pinned version, lockfile, package hashes, or integrity verification. The emitted installation instruction resolves whichever version is current from the user's configured Python package index at installation time.

This does not establish that yfinance is malicious. The risk is that installations are mutable and non-reproducible: a future compromised release, compromised package index, maliciously configured mirror, dependency-resolution change, or unsafe transitive dependency could introduce code that was not reviewed with this Skill. Imported package code executes in the Skill's Python process and inherits its network and filesystem access.

Attack Path

  1. A user runs the screener without yfinance installed.
  2. The program displays pip install yfinance.
  3. The user follows the instruction without a version constraint or hash requirement.
  4. pip resolves the package and transitive dependencies from the configured package index or mirror.
  5. If a selected package artifact or dependency has been compromised, its installation hooks or imported code execute with the user's privileges.
  6. The compromised dependency can access files, environment variables, network resources, and process data available to the screener process.

Exploitation depends on compromise or malicious configuration of the dependency supply chain; no such compromise was de ...[truncated 594 chars]

Remediation
View remediation

Remediation Suggestions

  1. Declare an explicitly reviewed yfinance version in a dependency manifest.
  2. Generate and commit a lockfile containing exact versions for all transitive dependencies.
  3. Require package hashes during installation, for example through a hash-locked requirements file and pip install --require-hashes.
  4. Install only from trusted HTTPS package indexes and prohibit unexpected extra indexes or untrusted mirrors in deployment configuration.
  5. Perform automated dependency vulnerability and provenance scanning in CI.
  6. Review updates before changing locked versions, and rebuild lockfiles in a controlled environment.
  7. Run the Skill in a least-privileged virtual environment or container with only the filesystem and network access required for market-data retrieval and watchlist storage.
  8. Replace the informal runtime installation message with documented, reproducible setup commands tied to the locked dependency manifest.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (8)

Context-Inappropriate Capability

High
Category
Not specified by scanner
Confidence
96% confidence
Finding

The charge method performs a live external payment API call that can bill a user by identifier, yet the skill context provided does not justify payment processing or network-based charging. This creates a direct risk of unauthorized financial transactions and makes the skill materially more dangerous because the functionality is actionable, not merely preparatory.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The file loads billing credentials from config or environment and is clearly designed to support payment operations, despite no accompanying permission, consent, or declared billing scope in the provided skill context. In an agent skill, introducing monetization and payment credential handling without explicit disclosure increases the risk of unauthorized charging behavior and covert financial functionality.

Content

No source excerpt is available for this finding.

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 · scripts/billing.py (reported line 26)May include surrounding context.

python
def __init__(self, api_key: str = None, skill_id: str = None):
        self.api_key = api_key or SKILLPAY_API_KEY
        self.skill_id = skill_id or SKILL_ID
        self.base_url = "https://api.skillpay.me/v1"
        
        if not self.api_key:
            raise ValueError("SkillPay API key required")

External Transmission

Medium
Category
Data Exfiltration
Confidence
93% confidence
Finding

This request sends data to an external payment endpoint, including billing-related information and identifiers, creating a clear external transmission channel. In the absence of declared billing purpose or user-facing disclosure, that transmission is risky because it can move sensitive operational and user data to a third party unexpectedly.

Content

Scanner excerpt · scripts/billing.py (reported line 36)May include surrounding context.

python
amount = amount or DEFAULT_PRICE
        
        try:
            response = requests.post(
                f"{self.base_url}/charge",
                json={
                    "api_key": self.api_key,

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The code transmits both a user identifier and the billing API key to a remote service without any visible notice, consent, or privacy explanation in this file. Sending credentials and user-linked data off-platform without disclosure can expose sensitive information, complicate compliance, and enable misuse if the endpoint or surrounding controls are weak.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
99% confidence
Finding

A live-looking secret API key is hardcoded directly in source code, which makes it trivially recoverable by anyone with repository or package access. Exposed billing or payment-related API credentials can enable unauthorized charges, account abuse, service impersonation, and persistent compromise until the key is revoked.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
88% confidence
Finding

Accessing a sensitive billing credential from the environment is not inherently unsafe, but in this skill it supports undisclosed payment functionality and there is no visible transparency around credential use. In this context, the issue is less about the environment variable itself and more about secret use for hidden monetization behavior.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
84% confidence
Finding

This code persists stock results to a JSON file under the user's home directory, which affects local user data. Although the function prints after saving, there is no disclosure before the write in the CLI help, no confirmation prompt, and no warning comment/docstring at the command entry point that using --save will create files on disk.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.