Back to skill

Security audit

finviz-crawler

Security checks for vulnerabilities and agentic risk

Overview

This skill has a coherent financial-news-crawler purpose, but it installs persistent background execution by default and contains unsafe file and browser-crawling behavior that needs user review before installation.

Install only if you are comfortable with a user-level background crawler that persists across logins, downloads browser components, scrapes third-party sites, and writes/deletes local article files. Review or patch ticker validation, service opt-in/uninstall behavior, dependency pinning, and the Chromium sandbox setting before using it on a machine with sensitive files or private network access.

Vulnerability Patterns
  • System PersistenceInstalls backdoors, hooks, services, or scheduled tasks that survive the run
  • 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
Findings (6)

T06 · System Persistence

Error
Location
scripts/install.py:75
Finding

Installer Automatically Enables Cross-Session Background Persistence

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/finviz_query.py:66
Finding

Unvalidated Ticker Values Can Cause Recursive Deletion Outside the Article Directory

Content
View full analysis
None: conn = get_conn(db) now = datetime.now(timezone.utc).isoformat() added = [] for spec in specs: # Format: "NVDA" or "NVDA:nvidia,jensen huang" if ":" in spec: sym, kw_str = spec.split(":", 1) keywords = [k.strip() for k in kw_str.split(",") if k.strip()] else: sym = spec.strip() keywords = [sym.lower()] sym = sym.strip().upper() if not sym: continue conn.execute( "INSERT OR REPLACE INTO tickers (symbol, keywords, added_at) VALUES (?, ?, ?)", (sym, json.dumps(keywords), now), ) ``` ```python def remove_tickers(db: str, symbols: list[str], articles_db: str = DEFAULT_DB, articles_dir: str = DEFAULT_ARTICLES_DIR) -> None: conn = get_conn(db) removed = [] for sym in symbols: sym = sym.strip().upper() cur = conn.execute("DELETE FROM tickers WHERE symbol = ?", (sym,)) if cur.rowcount: removed.append(sym) conn.commit() conn.close() if not removed: print("No matching tickers found") return # Delete articles from finviz DB and disk if os.path.exists(articles_db): fconn = sqlite3.connect(articles_db) fconn.row_factory = sqlite3.Row for sym in removed: # Delete files from disk (subfolder) subfolder = os.path.join(articles_dir, sym.lower()) file_count = 0 if os.path.isdir(subfolder): import shutil file_count = len([f for f in os.listdir(subfolder) if f.endswith(".md")]) shutil.rmtree(subfolder) ``` ### Technical Analysis Ticker input is converted to uppercase ...[truncated 1845 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/finviz_crawler.py:392
Finding

Ticker Path Traversal Allows Article Writes Outside the Configured Storage Root

Content
View full analysis
str: """Save article as .md file in ticker subfolder. Returns relative path (ticker/filename).""" subfolder = (ticker or "market").lower() target_dir = os.path.join(articles_dir, subfolder) Path(target_dir).mkdir(parents=True, exist_ok=True) filename = sanitize_filename(title) + ".md" filepath = os.path.join(target_dir, filename) if os.path.exists(filepath): with open(filepath, "r", errors="replace") as f: first_line = f.readline() if title.lower() not in first_line.lower(): filename = f"{sanitize_filename(title)}_{title_hash(title)[:8]}.md" filepath = os.path.join(target_dir, filename) with open(filepath, "w") as f: f.write(f"# {title}\n\n") f.write(f"- **URL:** {url}\n") f.write(f"- **Source:** {domain}\n") f.write(f"- **Published:** {publish_at}\n") f.write(f"- **Crawled:** {now_seattle()}\n\n") f.write("---\n\n") f.write(content) ``` ### Technical Analysis The ticker value is used directly as a directory component. It is not validated before `os.path.join()` and `Path.mkdir()`. Absolute paths can replace `articles_dir`, and traversal components can move the destination outside the expected root. The resulting Markdown file is opened with mode `"w"`, which creates a file or truncates an existing file with the same generated name. Although the article title is sanitized before becoming a filename, that does not protect the unvalidated directory component. ### Attack Path 1. A path-shaped ticker is added to the ticker database. 2. `_load_tickers()` reads that ticker without valid ...[truncated 860 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/finviz_crawler.py:471
Finding

Remote Headline Links Are Crawled Without SSRF Destination Validation

Content
View full analysis
list[dict]: items = [] seen_titles = set() pattern = re.compile( r'(\d{1,2}:\d{2}(?:AM|PM)).*?' r']+href="([^"]+)"[^>]*class="[^"]*nn-tab-link[^"]*"[^>]*>\s*(.+?)\s*', re.DOTALL | re.IGNORECASE, ) for m in pattern.finditer(html): time_str, url, raw_title = m.group(1), m.group(2), m.group(3) title = re.sub(r"<[^>]+>", "", raw_title).strip() domain = extract_domain(url) norm = title.lower().strip() if not title or len(title) < 10 or norm in seen_titles or domain in AD_DOMAINS: continue seen_titles.add(norm) items.append({"time": time_str, "title": title, "url": url, "source": domain, "domain": domain}) ``` ```python url = article["url"] title = article["title"] domain = article["domain"] try: config = CrawlerRunConfig( only_text=True, check_robots_txt=True, page_timeout=20000, wait_until="domcontentloaded", user_agent=ua, exclude_all_images=True, exclude_external_links=True, remove_overlay_elements=True, verbose=False, ) t0 = time.monotonic() result = await crawler.arun(url=url, config=config) ``` ### Technical Analysis Article links are parsed from remote Finviz HTML and later supplied directly to a browser crawler. The primary parser accepts any `href` string and does not require HTTP or HTTPS. Neither parser nor crawler verifies the destination host, resolved IP address, port, redirect chain, or URL credentials. `check_robots_txt` is ...[truncated 1578 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/finviz_crawler.py:709
Finding

Chromium Sandbox Is Disabled While Rendering Untrusted Third-Party Pages

Content
View full analysis
Remediation
View remediation

T08 · Insecure Dependencies

Warning
Location
scripts/install.py:30
Finding

Installer Retrieves and Executes Unpinned Third-Party Packages and Browser Components

Content
View full analysis
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (36)

Tp4

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding

The documented behavior goes beyond a passive news crawler and query tool by including mutation of the database and destructive deletion of article files when removing tickers, while also omitting actual evidence of crawler behavior in this file. This mismatch is dangerous because users or orchestration systems may grant trust based on the stated purpose while the skill performs broader state-changing actions than expected.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

High
Category
Not specified by scanner
Confidence
97% confidence
Finding

The script is described and named as a read/query utility for summarization, but it also exposes state-changing management operations including adding/removing tickers and, via removal, deleting stored articles from both SQLite and disk. This capability mismatch is dangerous because operators or higher-level agents may invoke it under the assumption that it is read-only, enabling unintended destructive actions in automation contexts such as cron jobs or agent toolchains.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

High
Category
Not specified by scanner
Confidence
99% confidence
Finding

The remove_tickers function deletes database rows from the articles table and recursively removes article files from disk, which goes well beyond simple ticker untracking. In an agent skill advertised as a query tool for local news data, this creates a high-risk destructive primitive that can cause permanent data loss if misused, invoked unexpectedly, or targeted with the wrong ticker set.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The README states that installation will create persistent storage under the user's home directory and set up a background service, but it does not prominently warn that the install process makes ongoing system changes beyond a one-time local setup. For an agent skill, this can lead users to authorize installation without understanding that a continuously running service and retained data will persist after the initial command, which increases the risk of unintended resource use, privacy exposure, and difficult-to-notice long-lived behavior.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding

The skill advertises installation and operation of Python scripts that can read/write local files, access the network, inspect environment settings, and run shell commands, but it does not declare any explicit tool scope or permissions boundary. In an agent ecosystem, this broad undeclared capability increases the chance of accidental overreach or misuse because operators cannot easily constrain what the skill is allowed to do.

Content

No source excerpt is available for this finding.

Autonomous Decision Making

Medium
Category
Excessive Agency
Confidence
80% confidence
Finding

Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Content

Scanner excerpt · SKILL.md (reported line 17)May include surrounding context.

md
🤖 **Built for AI summarization** — the query tool outputs clean text/JSON optimized for LLM digests. Pair with an OpenClaw cron job for automated morning briefings, evening wrap-ups, or weekly investment summaries.

💾 **Auto-cleanup** — configurable expiry automatically deletes old articles from both the database and disk. Set `--expiry-days 30` to keep a month of history, or `0` to keep everything forever.

🔄 **Daemon architecture** — runs as a background service that starts/stops with OpenClaw. No manual intervention needed after setup. Works with systemd (Linux) and launchd (macOS).

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
98% confidence
Finding

The architecture section describes the query tool as read-only, but earlier documented commands allow adding and removing tickers in the SQLite database. This contradiction can mislead users and higher-level agents into treating a mutating tool as safe for read-only contexts, increasing the risk of unauthorized or unintended data changes.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
75% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · SKILL.md (reported line 167)May include surrounding context.

launchd (macOS)

xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key><string>com.finviz.crawler</string>

Session Persistence

Medium
Category
Rogue Agent
Confidence
75% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · SKILL.md (reported line 168)May include surrounding context.

launchd (macOS)

xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key><string>com.finviz.crawler</string>

Session Persistence

Medium
Category
Rogue Agent
Confidence
75% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · SKILL.md (reported line 181)May include surrounding context.

launchd (macOS)

xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key><string>com.finviz.crawler</string>

Session Persistence

Medium
Category
Rogue Agent
Confidence
75% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · SKILL.md (reported line 167)May include surrounding context.

launchd (macOS)

xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key><string>com.finviz.crawler</string>

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The code deletes stored .md files and corresponding SQLite records in expire_old_articles, and this behavior is only implied by implementation details and the --expiry-days option text. There is no explicit runtime warning, confirmation prompt, or broader user disclosure near startup that retained data will be automatically purged.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
98% confidence
Finding

Irreversible deletion of files and database rows occurs immediately with no confirmation prompt, no --force gate, and no warning summarizing the scope of data to be removed. In unattended or agent-driven environments, this sharply increases the chance of accidental destructive execution and makes recovery difficult or impossible.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/install.py (reported line 27)May include surrounding context.

python
def run(cmd, check=True):
    print(f"  → {' '.join(cmd)}")
    return subprocess.run(cmd, check=check, capture_output=True, text=True)


def pip_install(packages):

Session Persistence

Medium
Category
Rogue Agent
Confidence
60% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · scripts/install.py (reported line 43)May include surrounding context.

python
def setup_tickers_db(settings_dir):
    """Create tickers SQLite DB with default tickers."""
    os.makedirs(settings_dir, exist_ok=True)
    db_path = os.path.join(settings_dir, "finviz.db")
    conn = sqlite3.connect(db_path)

Session Persistence

Medium
Category
Rogue Agent
Confidence
96% confidence
Finding

This code creates a launchd LaunchAgent for automatic execution at login, which is a real persistence mechanism even if used for a legitimate background crawler. Persistence increases risk because compromised or modified crawler code would be re-executed automatically on future logins, and users may not realize the install script is establishing autorun behavior.

Content

Scanner excerpt · scripts/install.py (reported line 102)May include surrounding context.

python
print("  Start now with: systemctl --user start finviz-crawler.service")


def setup_launchd_plist(script_dir, python_exe):
    """Create launchd plist (macOS only)."""
    plist_dir = os.path.expanduser("~/Library/LaunchAgents")
    os.makedirs(plist_dir, exist_ok=True)

Session Persistence

Medium
Category
Rogue Agent
Confidence
96% confidence
Finding

The function sets up a LaunchAgents directory specifically for launchd-based persistence on macOS. While consistent with the skill's daemon/service context, it still establishes automatic execution and therefore meaningfully enlarges the blast radius of any later code compromise or tampering.

Content

Scanner excerpt · scripts/install.py (reported line 103)May include surrounding context.

python
def setup_launchd_plist(script_dir, python_exe):
    """Create launchd plist (macOS only)."""
    plist_dir = os.path.expanduser("~/Library/LaunchAgents")
    os.makedirs(plist_dir, exist_ok=True)
    crawler_path = os.path.join(script_dir, "finviz_crawler.py")

Session Persistence

Medium
Category
Rogue Agent
Confidence
96% confidence
Finding

The installer prepares the crawler script path for inclusion in a persistent launchd configuration. Because this ties a local script to automatic execution, any unauthorized modification of that script would be repeatedly launched without further user action.

Content

Scanner excerpt · scripts/install.py (reported line 104)May include surrounding context.

python
def setup_launchd_plist(script_dir, python_exe):
    """Create launchd plist (macOS only)."""
    plist_dir = os.path.expanduser("~/Library/LaunchAgents")
    os.makedirs(plist_dir, exist_ok=True)
    crawler_path = os.path.join(script_dir, "finviz_crawler.py")
    log_path = os.path.expanduser("~/Library/Logs/finviz-crawler.log")

Session Persistence

Medium
Category
Rogue Agent
Confidence
95% confidence
Finding

The associated log path itself is not dangerous, but it is part of the broader LaunchAgent setup that creates persistence. In context, this line supports a login-triggered background service, which is a legitimate but still security-relevant persistence capability.

Content

Scanner excerpt · scripts/install.py (reported line 105)May include surrounding context.

python
def setup_launchd_plist(script_dir, python_exe):
    """Create launchd plist (macOS only)."""
    plist_dir = os.path.expanduser("~/Library/LaunchAgents")
    os.makedirs(plist_dir, exist_ok=True)
    crawler_path = os.path.join(script_dir, "finviz_crawler.py")
    log_path = os.path.expanduser("~/Library/Logs/finviz-crawler.log")

Session Persistence

Medium
Category
Rogue Agent
Confidence
97% confidence
Finding

The installer constructs the LaunchAgent plist content that will cause the crawler to run automatically. This is a true persistence mechanism; in a benign admin tool this may be expected, but from a security-analysis perspective it remains a notable capability because it can survive user sessions and repeatedly execute code.

Content

Scanner excerpt · scripts/install.py (reported line 109)May include surrounding context.

python
crawler_path = os.path.join(script_dir, "finviz_crawler.py")
    log_path = os.path.expanduser("~/Library/Logs/finviz-crawler.log")

    plist_content = f"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>

Session Persistence

Medium
Category
Rogue Agent
Confidence
97% confidence
Finding

The PLIST header is another instance of the same launchd persistence artifact. In the context of a background financial crawler, autorun is operationally understandable, but it is still a security-relevant persistence capability because it ensures code execution at login.

Content

Scanner excerpt · scripts/install.py (reported line 110)May include surrounding context.

python
log_path = os.path.expanduser("~/Library/Logs/finviz-crawler.log")

    plist_content = f"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key><string>com.finviz.crawler</string>

Session Persistence

Medium
Category
Rogue Agent
Confidence
97% confidence
Finding

The PLIST header is another instance of the same launchd persistence artifact. In the context of a background financial crawler, autorun is operationally understandable, but it is still a security-relevant persistence capability because it ensures code execution at login.

Content

Scanner excerpt · scripts/install.py (reported line 110)May include surrounding context.

python
log_path = os.path.expanduser("~/Library/Logs/finviz-crawler.log")

    plist_content = f"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key><string>com.finviz.crawler</string>

Session Persistence

Medium
Category
Rogue Agent
Confidence
97% confidence
Finding

The LaunchAgent label and program arguments define the persisted task that will run at login. This becomes dangerous if an attacker can modify the referenced interpreter, script, or surrounding environment, since launchd will then repeatedly execute the altered payload.

Content

Scanner excerpt · scripts/install.py (reported line 111)May include surrounding context.

python
plist_content = f"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key><string>com.finviz.crawler</string>
    <key>ProgramArguments</key>

Session Persistence

Medium
Category
Rogue Agent
Confidence
97% confidence
Finding

Writing the plist file to ~/Library/LaunchAgents is the concrete step that establishes macOS persistence. The skill explicitly aims to run as a background daemon, so this is contextually expected, but it still creates long-lived automatic execution that would amplify any downstream compromise.

Content

Scanner excerpt · scripts/install.py (reported line 126)May include surrounding context.

python
<key>StandardOutPath</key><string>{log_path}</string>
    <key>StandardErrorPath</key><string>{log_path}</string>
</dict>
</plist>"""
    plist_path = os.path.join(plist_dir, "com.finviz.crawler.plist")
    with open(plist_path, "w") as f:
        f.write(plist_content)

Session Persistence

Medium
Category
Rogue Agent
Confidence
96% confidence
Finding

The plist write operation persists the autorun configuration onto disk. Even benign persistence should be tracked because installers that modify LaunchAgents can be abused or overlooked by users, leading to durable execution of changed code later on.

Content

Scanner excerpt · scripts/install.py (reported line 127)May include surrounding context.

python
<key>StandardErrorPath</key><string>{log_path}</string>
</dict>
</plist>"""
    plist_path = os.path.join(plist_dir, "com.finviz.crawler.plist")
    with open(plist_path, "w") as f:
        f.write(plist_content)
    print(f"  📝 Plist file: {plist_path}")

Static analysis

No suspicious patterns detected.