Back to skill

Security audit

Podcast to Substack

Security checks for vulnerabilities and agentic risk

Overview

The skill matches a podcast publishing workflow, but it needs review because it can publish publicly, install a package at runtime, and download Notion-provided URLs without tight limits.

Review this skill before installing. Use it only in an environment where runtime package installation is disabled or pre-provisioned, Notion/image network egress is constrained, and a human explicitly approves any Substack publish or LinkedIn post. Prefer running Notion image fetching with --no-download unless the sources are trusted.

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)

T08 · Insecure Dependencies

Error
Location
scripts/fetch_rss.py:9
Finding

Unpinned dependency is installed automatically at runtime

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/fetch_rss.py:22
Finding

User-supplied RSS URLs permit unrestricted server-side network requests

Content
View full analysis
list: feed = feedparser.parse(rss_url) ``` ```python url = sys.argv[1] days = int(sys.argv[2]) if len(sys.argv) > 2 else 7 result = fetch_episodes(url, days) print(json.dumps(result, indent=2)) ``` ### Technical Analysis The command-line URL is passed directly to `feedparser.parse` without validating its scheme, hostname, resolved IP address, port, or redirect destinations. Consequently, the request is made from the Agent's network context rather than the requesting user's network context. Fetching a caller-selected RSS feed is part of the declared functionality. However, allowing arbitrary destinations—including loopback, private, link-local, reserved, and cloud metadata addresses—is broader than necessary. Redirects and DNS resolution must also be validated because a superficially acceptable public URL may redirect to, or resolve as, an internal address. The script prints parsed feed content, which can expose information returned by an internal HTTP service if that response is parseable as a feed. ### Attack Path 1. An attacker or untrusted workflow input supplies a URL targeting an internal service, loopback address, private network host, or cloud metadata endpoint. 2. The Skill invokes `fetch_rss.py` with that value as `RSS_URL`. 3. `feedparser.parse` initiates the request from the Agent's trusted execution environment. 4. The destination receives a request that may bypass external network boundaries. 5. If the response is parseable as RSS or Atom data, selected fields are serialized to standard output and may be returned to the attacker. 6. Even when the response is not parseable, the request can still be used for network probing or interaction with internal services. ### Impact ...[truncated 421 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/fetch_notion_episode.py:219
Finding

Notion-controlled external image URLs are downloaded without destination or size restrictions

Content
View full analysis
list[str]: urls: list[str] = [] # 1) Inline image blocks. for b in blocks: if b.get("type") != "image": continue image = b.get("image", {}) itype = image.get("type") if itype == "file": u = image.get("file", {}).get("url") elif itype == "external": u = image.get("external", {}).get("url") else: u = None if u: urls.append(u) # 2) Files properties on the page (covers thumbnail/cover-type properties). props = page.get("properties", {}) for p in props.values(): if not isinstance(p, dict) or p.get("type") != "files": continue for item in p.get("files", []): itype = item.get("type") if itype == "file": u = item.get("file", {}).get("url") elif itype == "external": u = item.get("external", {}).get("url") else: u = None if u: urls.append(u) ``` ```python def download_images(urls: list[str], out_dir: Path) -> list[str]: out_dir.mkdir(parents=True, exist_ok=True) local_paths: list[str] = [] for idx, u in enumerate(urls, start=1): req = urllib.request.Request(u, headers={"User-Agent": USER_AGENT}) try: with urllib.request.urlopen(req, timeout=40) as resp: content = resp.read() ext = infer_ext(u, resp.headers.get("Content-Type")) except Exception as exc: # pragma: no cover - best effort download print(f"warning: failed to download image {u}: {exc}", file=sys.stderr) continue ...[truncated 2704 chars]
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (13)

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/fetch_notion_episode.py (reported line 54)May include surrounding context.

python
},
    )
    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            raw = resp.read().decode("utf-8")
            return json.loads(raw)
    except urllib.error.HTTPError as exc:

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

Critical
Category
Data Flow
Confidence
92% confidence
Finding

The script downloads arbitrary image URLs returned from Notion page content or file properties without validating scheme, host, size, or content type. If an attacker can influence Notion content, this creates an SSRF-style outbound fetch primitive and can also lead to downloading unexpectedly large or non-image payloads to local disk.

Content

Scanner excerpt · scripts/fetch_notion_episode.py (reported line 278)May include surrounding context.

python
for idx, u in enumerate(urls, start=1):
        req = urllib.request.Request(u, headers={"User-Agent": USER_AGENT})
        try:
            with urllib.request.urlopen(req, timeout=40) as resp:
                content = resp.read()
                ext = infer_ext(u, resp.headers.get("Content-Type"))
        except Exception as exc:  # pragma: no cover - best effort download

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding

The declared description presents a broader end-to-end workflow: taking podcast data from RSS + Notion, publishing to Substack with reliable Apple Podcasts embedding, extracting images, and generating LinkedIn-ready companion posts. The supplied code only implements the Notion-fetch portion of that pipeline. It queries a Notion data source, selects a page, recursively extracts block text, finds image URLs, optionally downloads images, and emits JSON. While image extraction is consistent with part of the description, the major advertised capabilities—RSS handling, Substack publishing, embed-fix behavior, and LinkedIn post generation—are absent. Therefore the code chunk does not accurately represent the declared overall purpose.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

The declared description presents a multi-system automation workflow involving RSS, Notion, Substack publishing, Apple Podcasts embed reliability, image extraction, and LinkedIn post generation. The actual code chunk is much narrower: it parses an RSS feed using feedparser, filters entries by date, extracts episode numbers from titles, and outputs episode metadata as JSON. While RSS fetching is one supporting part of the stated workflow, the code does not implement the primary advertised behaviors or integrations. Therefore the description materially overstates what this code chunk actually does.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The declared description describes a multi-step workflow for transforming podcast content from RSS/Notion into Substack posts with embedding and LinkedIn cross-post generation. The supplied code does none of that. It only echoes a single Apple Podcasts URL from an environment variable or default constant. While Apple Podcasts is mentioned in both, the implementation is far narrower and materially different from the declared primary purpose, and it even ignores the episode_title input rather than producing episode-specific links.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The declared description presents a broader workflow skill centered on publishing podcast episodes from RSS/Notion to Substack, fixing image-fetch and Apple embed issues, and then creating LinkedIn companion posts. The supplied code chunk implements only the last, narrow piece: formatting a LinkedIn-ready post from provided text fields. It performs no network access, no RSS parsing, no Notion integration, no Substack interaction, and no Apple embed reliability logic. Because the actual code's primary purpose is much narrower and omits the core declared publishing/integration behavior, this is a material description-behavior mismatch.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding

The skill invokes shell commands, reads environment-based secrets, accesses local files, and depends on networked services, but it declares no explicit tool scope or permission boundaries. In an agent environment, this can lead to overbroad execution authority and unintended access to secrets, files, or external systems when the skill is run automatically.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The workflow includes publishing to Substack and posting or queueing LinkedIn content, but the skill text does not prominently warn that it can affect public-facing accounts. In agent-assisted environments, missing disclosure and confirmation requirements can cause unintended publication, reputational damage, and unauthorized changes to external platforms.

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/fetch_notion_episode.py (reported line 23)May include surrounding context.

python
from pathlib import Path
from typing import Any

API = "https://api.notion.com/v1"
NOTION_VERSION = "2025-09-03"
USER_AGENT = "podcast-to-substack-notion-fetcher/1.0"

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

Installing a Python package at runtime is dangerous because it performs network-dependent code acquisition and execution outside the skill's stated purpose of parsing RSS. In an agent or automation environment, this can lead to unreviewed dependency changes, supply-chain compromise, or unexpected mutation of the host environment.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The script automatically installs feedparser without warning or confirmation, so a caller expecting read-only RSS processing may instead trigger package installation and subprocess execution. That behavior is especially risky in shared, sandboxed, or production environments where unauthorized package changes can weaken isolation or break reproducibility.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
91% confidence
Finding

The script invokes pip at runtime via a subprocess when feedparser is missing, which introduces unintended code execution and network access during normal use. This expands the trust boundary from a simple RSS fetcher to arbitrary package installation, creating supply-chain and environment integrity risks if the package source, resolver, or execution context is compromised.

Content

Scanner excerpt · scripts/fetch_rss.py (reported line 13)May include surrounding context.

python
import feedparser
except ImportError:
    import subprocess
    subprocess.check_call([sys.executable, "-m", "pip", "install", "feedparser", "-q"])
    import feedparser

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
82% confidence
Finding

The skill states that it uses a Notion API key and fetches scripts and images from Notion, but it does not clearly warn that external content will be accessed and downloaded. This can expose users to unexpected data egress, handling of sensitive workspace content, or ingestion of untrusted remote assets without informed consent.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.