Back to skill

Security audit

DeepReader

Security checks for vulnerabilities and agentic risk

Overview

DeepReader is a disclosed web-ingestion skill, but it needs review because it automatically fetches arbitrary links and saves untrusted web content into agent memory.

Install only if you are comfortable with automatic URL fetching and memory writes. Prefer using it in an environment with outbound network controls that block localhost, private networks, and cloud metadata services; review or clear saved memory entries after ingestion; and consider pinning dependencies before production use.

Vulnerability Patterns
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • 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
  • 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 (2)

T09 · Insecure Skill Coding Practices

Error
Location
parsers/generic.py:64
Finding
Unrestricted URL Fetching Enables Server-Side Request Forgery<![CDATA[ ## Vulnerability Details **File Location**: `parsers/generic.py:64-73` and `core/utils.py:21-36` **Vulnerability Type**: Server-Side Request Forgery (SSRF) **Risk Level**: High ### Vulnerable Code URL extraction accepts any HTTP or HTTPS URL without validating whether its destination is public: ```python _URL_PATTERN = re.compile( r"https?://" # scheme r"(?:[a-zA-Z0-9\-._~:/?#\[\]@!$&'()*+,;=%])+", # rest of URI chars re.IGNORECASE, ) def extract_urls(text: str) -> list[str]: """Return a deduplicated, order-preserved list of URLs found in *text*.""" seen: set[str] = set() urls: list[str] = [] for match in _URL_PATTERN.finditer(text): url = match.group(0).rstrip(".,;:!?)") # strip trailing punctuation if url not in seen: seen.add(url) urls.append(url) return urls ``` The generic parser then fetches that destination and follows redirects automatically: ```python def _fetch_html(self, url: str) -> str | None: """Download the raw HTML content of *url*.""" response = requests.get( url, headers=self._get_headers(), timeout=self.timeout, allow_redirects=True, ) response.raise_for_status() return response.text ``` ### Technical Analysis Any URL that is not classified as Twitter, Reddit, or YouTube is passed to `GenericParser`. The implementation does not resolve and inspect the destination address before making the request. Consequently, an attacker can supply URLs targeting: - Loopback interfaces such as `127.0.0.1` or `::1` - RFC1918 private networks - Link-local addresses - Cloud instance metadata services - Internal DNS names - Reserved or otherwise non-public address ranges Setting `allow_redirects=True` also permits a public endpoint to redirect the request to a private destination. No validation is performed after a redirect or after DNS resolution, leaving the implementation exposed to redirect-ba ...[truncated 2019 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse each URL and permit only `http` and `https` schemes with a valid hostname. 2. Resolve all destination addresses before connecting. 3. Reject IPv4 and IPv6 addresses classified as loopback, private, link-local, multicast, reserved, unspecified, or non-global. 4. Explicitly block cloud metadata destinations, including link-local metadata addresses and provider-specific metadata hostnames. 5. Disable automatic redirects. If redirects are required, follow them manually and repeat scheme, hostname, DNS, and IP validation for every hop. 6. Protect against DNS rebinding by ensuring that the validated address is the address actually used for the connection. 7. Consider enforcing an allowlist or requiring explicit approval for destinations outside expected public domains. 8. Use streamed responses and enforce strict limits on: - Downloaded bytes - Decompressed bytes - Redirect count - URLs processed per invocation - Extracted and persisted content size 9. Apply outbound network controls at the runtime or container layer to prevent access to private networks and metadata services. 10. Return generic network errors rather than exposing detailed internal connection information. ]]>

T02 · Agent Memory Poisoning

Warning
Location
__init__.py:104
Finding
Automatic Persistence of Attacker-Controlled Web Content Enables Agent Memory Poisoning<![CDATA[ ## Vulnerability Details **File Location**: `__init__.py:104-121` and `core/storage.py:151-175` **Vulnerability Type**: Persistent agent memory poisoning through untrusted remote content **Risk Level**: Medium ### Vulnerable Code Every successfully parsed URL is saved automatically, without user confirmation or a trust decision: ```python for url in urls: logger.info("Processing URL: %s", url) # Step 2: Route to the correct parser parse_result = router.route(url) if not parse_result.success: error_msg = ( f"❌ Failed to read **{url}**\n" f" Reason: {parse_result.error}" ) errors.append(error_msg) logger.warning("Parse failed for %s: %s", url, parse_result.error) continue # Step 3: Save to memory try: filepath = storage.save(parse_result) ``` The extracted remote text is then inserted directly into the stored Markdown document: ```python # Heading lines.append(f"# {result.title or 'Untitled'}") lines.append("") # Summary section lines.append("## Summary") lines.append("") if excerpt: lines.append(f"> {excerpt}") else: lines.append("*(To be filled by the Agent)*") lines.append("") # Content section lines.append("## Content") lines.append("") lines.append(result.content) lines.append("") return "\n".join(lines) ``` The generated document is written into the configured agent memory directory: ```python # Write to disk filepath.write_text(markdown, encoding="utf-8") logger.info("Saved content to %s (%d bytes)", filepath, len(markdown)) return str(filepath) ``` ### Technical Analysis The content returned by webpages, tweets, Reddit posts, comments, video descriptions, and transcripts is attacker-controlled. The implementation treats a successful network retrieval as sufficient authorization to place that content in the agent's persistent memory inbox. No mechanism: - Separates untrusted retrieved data from trusted agent instructions - ...[truncated 2567 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Store retrieved material in a quarantined content repository rather than directly in an instruction-bearing agent memory inbox. 2. Require explicit user approval before promoting fetched content into long-term memory. 3. Mark every stored document with immutable provenance and trust metadata, including: - Original URL - Retrieval timestamp - Parser used - Untrusted/external classification - Redirect chain and final destination 4. Ensure all downstream prompts place retrieved content inside a clearly delimited data section and explicitly prohibit following instructions found within it. 5. Keep system and developer instructions structurally separate from retrieved documents; do not concatenate memory content into an instruction channel. 6. Scan for likely prompt-injection language and warn or quarantine suspicious documents. Such scanning should supplement, not replace, architectural isolation. 7. Provide a preview-only mode and make persistence opt-in. 8. Limit the lifetime and scope of imported content, and provide users with a clear deletion and review workflow. 9. Sanitize and serialize frontmatter using a proper YAML library rather than manual string interpolation. 10. Add integration tests demonstrating that instructions embedded in fetched content cannot alter downstream agent behavior or initiate tool calls. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (28)

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The declared description promises a broad reader for multiple specific platforms (X, Reddit, YouTube, and any webpage) that converts content into clean Markdown. The supplied code only covers a generic HTML webpage parsing path. It uses requests to download page HTML, trafilatura to extract text in TXT format, and BeautifulSoup heuristics as fallback. There is no evidence in this chunk of platform-specific handling for X, Reddit, or YouTube, and no transcript extraction logic. The output is explicitly configured as plain text rather than Markdown. While generic webpage extraction is consistent with part of the description ('any webpage'), the chunk materially underdelivers on the broader declared purpose, so this is a description-behavior mismatch.

Tp4

High
Category
MCP Tool Poisoning
Confidence
91% confidence
Finding
The declared description presents a broad multi-source web reader covering X, Reddit, YouTube, and any webpage. The supplied code chunk, however, is narrowly scoped to YouTube only: it validates YouTube URLs, extracts a video ID, fetches YouTube metadata from the page, obtains transcripts using youtube_transcript_api, and formats the result. There is no evidence in this chunk of handling Twitter/X, Reddit, or generic webpages. While the YouTube portion is consistent with the description, the overall declared purpose is materially broader than the actual behavior shown here, so this chunk does not accurately represent the full declared capability.

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill advertises network fetching and writing ingested content to agent memory, but it declares no explicit tool scope or permission boundaries. In an agent environment, missing scope declarations can allow broader-than-expected network and file-write behavior, making review, policy enforcement, and least-privilege controls harder.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The skill states that it reads web content, but does not prominently warn users that fetched content is saved into agent memory as Markdown files. This creates a privacy and data-governance risk because sensitive, malicious, or irrelevant remote content may be persisted unexpectedly and later influence agent behavior.

Vague Triggers

Medium
Confidence
96% confidence
Finding
Automatically triggering on any HTTP/HTTPS URL is overly broad and can cause the skill to fetch untrusted remote content without clear user intent. In an agent setting this raises the chance of prompt-injection exposure, unexpected network access, and silent persistence of attacker-controlled content into memory.

Description-Behavior Mismatch

Medium
Confidence
88% confidence
Finding
The manifest describes DeepReader as a web content reader that reads social posts, webpages, and transcripts into clean Markdown. In this file, the skill not only formats content but creates directories and writes Markdown files into the agent's long-term memory/inbox, which is a persistence side effect beyond simple reading/parsing behavior.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
The manifest advertises broad ingestion of webpages, social media, comments, and transcripts into agent memory but does not describe privacy boundaries, retention, or handling of potentially sensitive third-party content. In this context, omission of data-handling warnings is risky because the skill is explicitly designed for collection and persistence of external content, which can lead to accidental ingestion of personal, copyrighted, or confidential material.

Vague Triggers

Medium
Confidence
96% confidence
Finding
The trigger regex matches any message containing any HTTP(S) URL, so the skill can auto-invoke on a very broad set of conversations unrelated to intentional scraping or ingestion. In a skill that reads webpages and writes extracted content into memory, this increases the chance of unneeded collection, processing, and storage of sensitive or private data from arbitrary links shared in chat.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
This parser sends tweet-derived identifiers to a third-party service (api.fxtwitter.com) without any indication in this file that the user is informed or consented. Even if only the username and tweet ID are transmitted, that still leaks user-requested browsing targets and associated metadata to an external operator, which is a real privacy issue for an agent skill whose purpose is to ingest arbitrary links.

External Transmission

Medium
Category
Data Exfiltration
Content
self, original_url: str, username: str, tweet_id: str,
    ) -> ParseResult:
        """Fetch tweet via the FxTwitter public JSON API."""
        api_url = f"https://api.fxtwitter.com/{username}/status/{tweet_id}"

        max_attempts = 2
        last_error = ""
Confidence
94% confidence
Finding
The hardcoded transmission target https://api.fxtwitter.com/ confirms that tweet requests are sent off-platform to an external service. In the context of a content-ingestion skill, this is risky because the agent may process sensitive or private investigative targets, and those access patterns become visible to the third-party API operator.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
The fallback logic sends requests to randomly selected public Nitter instances, meaning user-requested tweet paths are disclosed to unknown community-run servers. This increases privacy and trust risk beyond the primary backend because the destination varies, operators are not controlled, and reply-thread scraping may expose user activity patterns without any visible warning in this code.

Intent-Code Divergence

Low
Confidence
76% confidence
Finding
The top-level docstring frames the component as responsible for formatting extracted content into Markdown with YAML frontmatter, which understates the material behavior implemented below: directory creation and file persistence to the agent's memory area. While not wildly inconsistent, the documentation omits and thereby misrepresents the key side effect of writing to long-term memory.

Natural-Language Policy Violations

Low
Confidence
95% confidence
Finding
The default headers hard-code `Accept-Language` to `en-US,en;q=0.9`, which imposes a specific language/locale preference for all requests. This is a natural-language policy concern because the file does not offer user opt-in, configuration, or justification for forcing English.

Missing User Warnings

Low
Confidence
82% confidence
Finding
The parser performs a network request to the provided YouTube URL via requests.get, which transmits the user's target URL and system request metadata to an external service. While the module docstring mentions fetching metadata and transcripts, there is no user-facing warning, confirmation, or explicit disclosure near the request itself about contacting YouTube.

Unpinned Dependencies

Low
Category
Supply Chain
Content
# DeepReader Skill - Dependencies
# Web scraping & content extraction
trafilatura>=1.12.0
requests>=2.31.0
lxml>=5.1.0
lxml-html-clean>=0.4.0
Confidence
97% confidence
Finding
The dependency is specified with a lower-bound only, so builds may resolve to different versions over time. That weakens supply-chain reproducibility and makes it harder to ensure a reviewed, non-vulnerable version is consistently installed.

Unpinned Dependencies

Low
Category
Supply Chain
Content
# DeepReader Skill - Dependencies
# Web scraping & content extraction
trafilatura>=1.12.0
requests>=2.31.0
lxml>=5.1.0
lxml-html-clean>=0.4.0
Confidence
99% confidence
Finding
Using requests with only a minimum version allows environment-dependent resolution to potentially vulnerable or behavior-changing releases. In a web-ingestion skill, network-facing libraries are security-sensitive, so non-reproducible installs increase supply-chain and patch-verification risk.

Unverifiable Dependency: requests has 16 known advisory(ies) (CVE-2014-1830 (Exposure of Sensitive Information to an Unauthorized Actor in Requests); CVE-2024-47081 (Requests vulnerable to .netrc credentials leak via malicious URLs); CVE-2024-35195 (Requests `Session` object does not verify requests after making first request wi) +13 more), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
98% confidence
Finding
requests has multiple known advisories, and because the manifest does not pin a specific version, it is impossible to verify that deployed environments are using a fixed release. In this skill, requests is directly relevant because the tool fetches arbitrary remote content, increasing exposure to network-library flaws such as credential leakage or verification issues.

Unpinned Dependencies

Low
Category
Supply Chain
Content
# Web scraping & content extraction
trafilatura>=1.12.0
requests>=2.31.0
lxml>=5.1.0
lxml-html-clean>=0.4.0

# YouTube transcript extraction
Confidence
99% confidence
Finding
lxml is a high-risk parser component and is specified without an exact version, so installations may vary and could include releases with parser or sanitizer flaws. Because this skill processes arbitrary remote HTML, parser security matters more here than in a non-networked context.

Unverifiable Dependency: lxml has 14 known advisory(ies) (CVE-2021-43818 (lxml's HTML Cleaner allows crafted and SVG embedded scripts to pass through); CVE-2014-3146 (lxml Cross-site Scripting Via Control Characters); CVE-2021-28957 (lxml vulnerable to Cross-Site Scripting ) +11 more), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
99% confidence
Finding
lxml has a history of security issues, and the lack of version pinning means vulnerable parser/sanitizer behavior may still be present at deployment time. This is more dangerous in this skill because it ingests and parses untrusted web pages at scale, making malformed or malicious markup a realistic attack vector.

Unpinned Dependencies

Low
Category
Supply Chain
Content
trafilatura>=1.12.0
requests>=2.31.0
lxml>=5.1.0
lxml-html-clean>=0.4.0

# YouTube transcript extraction
youtube-transcript-api>=0.6.2
Confidence
99% confidence
Finding
lxml-html-clean is a sanitizer/cleaning component with only a minimum version constraint, leaving deployed versions non-deterministic. For a skill that cleans untrusted web content, sanitizer bypasses could allow unsafe content to survive filtering and affect downstream consumers.

Unverifiable Dependency: lxml-html-clean has 8 known advisory(ies) (CVE-2026-49825 (`lxml_html_clean.Cleaner` does not strip `javascript:` URLs from namespaced URL ); CVE-2024-52595 (HTML Cleaner allows crafted scripts in special contexts like svg or math to pass); CVE-2026-28348 (lxml-html-clean has CSS @import Filter Bypass via Unicode Escapes) +5 more), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
99% confidence
Finding
lxml-html-clean has known sanitizer bypass advisories, and without exact pinning there is no assurance the installed version contains the necessary fixes. Since the skill's purpose is to convert hostile web content into clean Markdown, sanitizer weaknesses are especially relevant and could allow script-bearing or deceptive content to survive processing.

Unpinned Dependencies

Low
Category
Supply Chain
Content
lxml-html-clean>=0.4.0

# YouTube transcript extraction
youtube-transcript-api>=0.6.2

# Data validation & settings
pydantic>=2.5.0
Confidence
95% confidence
Finding
The YouTube transcript library is not pinned, so the installed version may drift over time and introduce security or integrity issues without review. While less inherently risky than an HTML parser, it still expands the supply-chain attack surface.

Unpinned Dependencies

Low
Category
Supply Chain
Content
youtube-transcript-api>=0.6.2

# Data validation & settings
pydantic>=2.5.0
pydantic-settings>=2.1.0

# URL parsing & utilities
Confidence
95% confidence
Finding
pydantic is specified with a lower bound only, making dependency resolution non-reproducible and complicating assurance that vulnerable releases are excluded. Validation libraries can also be exposed to denial-of-service or parsing issues when handling untrusted input.

Unverifiable Dependency: pydantic has 4 known advisory(ies) (CVE-2021-29510 (Use of "infinity" as an input to datetime and date fields causes infinite loop i); CVE-2024-3772 (Pydantic regular expression denial of service); CVE-2021-29510 (Pydantic is a data validation and settings management using Python type hinting.) +1 more), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
96% confidence
Finding
pydantic has known advisories and the unpinned requirement makes it unverifiable whether a safe version will be installed. While the likely consequence is lower than parser-library issues, vulnerable validation code can still enable denial of service or unexpected parsing behavior on attacker-controlled data.

Unpinned Dependencies

Low
Category
Supply Chain
Content
# Data validation & settings
pydantic>=2.5.0
pydantic-settings>=2.1.0

# URL parsing & utilities
tldextract>=5.1.0
Confidence
93% confidence
Finding
pydantic-settings is unpinned, which means configuration-related behavior can change across installs and vulnerable versions cannot be confidently excluded. Even if not directly internet-facing, configuration loaders can affect secrets handling and trusted boundaries.

Static analysis

No suspicious patterns detected.