Back to skill

Security audit

daily-news-english-cards

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly does what it claims, but it also auto-installs mutable dependencies, loads nearby .env files, follows model-supplied image URLs, and adds undisclosed branding to every generated card.

Review before installing. Use this only in an isolated environment with trusted API keys, preinstall pinned dependencies yourself, avoid running it from directories containing unrelated .env files, and be aware that generated cards include undisclosed fixed branding unless the code is changed.

Vulnerability Patterns
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • 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)

T01 · Skill Instruction Hijacking

Error
Location
scripts/generate_cards.py:1165
Finding

Undisclosed Persistent Branding Injected into Generated Cards

Content
View full analysis

Vulnerability Details

File Location: scripts/generate_cards.py:1165-1195
Vulnerability Type: Undisclosed output manipulation
Risk Level: High

Vulnerable Code

python
# -- Brand logo: 萱媛背单词 (at comic/text boundary) --
_draw_brand_logo(draw, card, accent)

# ── Brand logo ──

def _draw_brand_logo(
    draw: ImageDraw.ImageDraw, card: Image.Image, accent: tuple,
):
    """Draw '萱媛背单词' brand logo at comic/text boundary line."""
    logo_icon = "\u2606"
    logo_text = " 萱媛背单词 "
    full_text = f"{logo_icon}{logo_text}{logo_icon}"

    logo_cjk_font = get_font(22, cjk=True)

    tb = draw.textbbox((0, 0), full_text, font=logo_cjk_font)
    tw = tb[2] - tb[0]
    th = tb[3] - tb[1]
    t_top = tb[1]

    pad_x, pad_y = 18, 8
    pill_w = tw + pad_x * 2
    pill_h = th + pad_y * 2

    logo_x = (CARD_W - pill_w) // 2
    logo_y = COMIC_H - pill_h // 2

Technical Analysis

The card-compositing pipeline unconditionally calls _draw_brand_logo() and embeds a fixed third-party brand into every final image. This behavior is not disclosed in SKILL.md, is not required to search news, generate educational content, create illustrations, or composite cards, and cannot be disabled through a documented option.

The behavior manipulates the final user-visible artifact beyond the Skill's declared functionality. Although it does not alter system permissions or execute code, it causes users to unknowingly produce and potentially redistribute branded material.

Attack Path

  1. A user invokes the Skill to generate news-based English learning cards.
  2. The Skill completes the news, text-generation, and image-generation phases.
  3. composite_card() unconditionally calls _draw_brand_logo().
  4. The fixed branding is rendered into every final PNG.
  5. The user may publish or distribute the output without knowing that third-party branding was inserted.

Impact Assessment

Th ...[truncated 283 chars]

Remediation
View remediation

Remediation Suggestions

  1. Remove the fixed branding from the default card-compositing path.
  2. If branding is a legitimate product requirement, disclose it prominently in SKILL.md.
  3. Make branding explicitly opt-in, such as through a --brand-text or --include-branding CLI option.
  4. Do not provide a non-empty default brand value.
  5. Add tests verifying that default outputs contain only user-requested or clearly documented content.

T08 · Insecure Dependencies

Warning
Location
scripts/generate_cards.py:49
Finding

Automatic Installation of Unpinned Dependencies at Runtime

Content
View full analysis

Vulnerability Details

File Location: scripts/generate_cards.py:49-67; dependency constraints in requirements.txt:1-4
Vulnerability Type: Mutable dependency installation and supply-chain exposure
Risk Level: Medium

Vulnerable Code

python
def _ensure_deps():
    required = {
        "tavily": "tavily-python",
        "openai": "openai",
        "PIL": "Pillow",
        "requests": "requests",
    }
    missing = []
    for mod, pkg in required.items():
        try:
            __import__(mod)
        except ImportError:
            missing.append(pkg)
    if missing:
        print(f"[setup] Installing missing packages: {', '.join(missing)}")
        subprocess.check_call(
            [sys.executable, "-m", "pip", "install", "-q"] + missing
        )


_ensure_deps()

The corresponding dependency file contains open-ended version constraints:

text
tavily-python>=0.5.0
openai>=1.0.0
Pillow>=10.0.0
requests>=2.28.0

Technical Analysis

The script invokes _ensure_deps() at module initialization. If any expected module is absent, it executes pip install automatically and without explicit confirmation. The installation command uses package names without exact versions or integrity hashes.

Consequently, the effective code installed and executed can change after the Skill has been reviewed. A compromised upstream release, dependency account takeover, malicious transitive dependency, or incompatible future release could execute code during installation or when imported.

The subprocess call uses a list rather than a shell command, so no direct shell-injection issue was found. The principal risk is uncontrolled supply-chain mutation and execution under the current Python environment.

Attack Path

  1. The user runs or imports the script in an environment where one or more dependencies are missing.
  2. _ensure_deps() detects the missing module.

...[truncated 838 chars]

Remediation
View remediation

Remediation Suggestions

  1. Remove automatic dependency installation from module initialization.
  2. Require dependencies to be installed through an explicit, documented setup step.
  3. Pin every direct dependency to an exact reviewed version.
  4. Use a lock file that also fixes transitive dependency versions.
  5. Require cryptographic hashes, such as with pip install --require-hashes.
  6. Install dependencies in an isolated virtual environment with minimal permissions.
  7. Use a trusted package index and consider blocking fallback to unapproved indexes.
  8. Run vulnerability and provenance checks against every dependency update.
  9. If installation assistance is retained, obtain explicit user consent before invoking pip and display the exact packages and versions.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/generate_cards.py:785
Finding

Model-Controlled Image URLs Are Retrieved Without Destination or Size Validation

Content
View full analysis

Vulnerability Details

File Location: scripts/generate_cards.py:785-843
Vulnerability Type: Server-side request forgery and unbounded remote-content processing
Risk Level: Medium

Vulnerable Code

python
def _call_openrouter(client: OpenAI, model: str, prompt: str) -> Image.Image | None:
    resp = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt}],
        extra_body={"modalities": ["image", "text"]},
    )

    msg = resp.choices[0].message

    # Case 1: images in model_extra (Gemini via OpenRouter)
    extras = getattr(msg, "model_extra", None) or {}
    images = extras.get("images")
    if images and isinstance(images, list):
        for img_part in images:
            if isinstance(img_part, dict) and img_part.get("type") == "image_url":
                url = img_part["image_url"]["url"]
                return _decode_image_url(url)

    # Case 2: content is a list of parts (multimodal response)
    if isinstance(msg.content, list):
        for part in msg.content:
            if isinstance(part, dict):
                if part.get("type") == "image_url":
                    url = part["image_url"]["url"]
                    return _decode_image_url(url)
                elif part.get("type") == "image":
                    b64 = part.get("data") or part.get("image", "")
                    return _decode_b64(b64)

    # Case 3: content is a string – look for inline base64 or URL
    text = msg.content if isinstance(msg.content, str) else str(msg.content or "")

    b64_match = re.search(
        r"data:image/(?:png|jpeg|webp);base64,([A-Za-z0-9+/=]+)", text
    )
    if b64_match:
        return _decode_b64(b64_match.group(1))

    url_match = re.search(r"(https?://\S+\.(?:png|jpg|jpeg|webp))", text)
    if url_match:
        return _download_image(url_match.group(1))

    raise ValueError("No imag
...[truncated 3175 chars]
Remediation
View remediation

Remediation Suggestions

  1. Prefer inline image data returned directly by the approved API rather than following arbitrary URLs.
  2. Maintain an explicit allowlist of trusted HTTPS image-hosting domains.
  3. Resolve the hostname before connecting and reject loopback, private, link-local, multicast, unspecified, and reserved IP ranges.
  4. Revalidate the hostname and resolved address after every redirect, or disable redirects entirely.
  5. Stream responses and terminate the transfer when a strict maximum size is exceeded.
  6. Require an approved image MIME type and reject ambiguous or missing content types.
  7. Verify image signatures independently of URL extensions and MIME headers.
  8. Set strict maximum pixel dimensions and preserve Pillow's decompression-bomb protections.
  9. Keep Pillow pinned to a reviewed, supported security release.
  10. Run image decoding in a resource-constrained subprocess or sandbox when processing remotely supplied content.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • System Prompt LeakageDirect Leakage, Indirect Extraction, Tool-Based Exfiltration
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (16)

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/generate_cards.py (reported line 26)May include surrounding context.

python
def _load_dotenv():
    """Load .env file from script dir, parent dir, or cwd (no external deps)."""
    candidates = [
        Path(__file__).resolve().parent.parent / ".env",
        Path(__file__).resolve().parent / ".env",

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/generate_cards.py (reported line 28)May include surrounding context.

python
def _load_dotenv():
    """Load .env file from script dir, parent dir, or cwd (no external deps)."""
    candidates = [
        Path(__file__).resolve().parent.parent / ".env",
        Path(__file__).resolve().parent / ".env",
        Path.cwd() / ".env",
    ]

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/generate_cards.py (reported line 29)May include surrounding context.

python
def _load_dotenv():
    """Load .env file from script dir, parent dir, or cwd (no external deps)."""
    candidates = [
        Path(__file__).resolve().parent.parent / ".env",
        Path(__file__).resolve().parent / ".env",
        Path.cwd() / ".env",
    ]

Credential Access

High
Category
Privilege Escalation
Confidence
77% confidence
Finding

Loading .env from the current working directory is more dangerous in an agent context because the working directory may be attacker-controlled or contain an unexpected .env file. That can let an attacker inject API endpoints, model selections, or replacement credentials used by the process, causing confused-deputy behavior, unauthorized billing, or data to be sent under attacker-chosen accounts.

Content

Scanner excerpt · scripts/generate_cards.py (reported line 30)May include surrounding context.

python
candidates = [
        Path(__file__).resolve().parent.parent / ".env",
        Path(__file__).resolve().parent / ".env",
        Path.cwd() / ".env",
    ]
    for env_path in candidates:
        if env_path.is_file():

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The trigger list ends with a broad catch-all phrase ('or similar requests related to news-based English learning content'), which can cause the skill to activate for loosely related prompts outside the user's clear intent. Overbroad activation is risky because this skill performs external API calls and content generation, so accidental triggering can send user requests and derived content to third-party services without sufficiently explicit consent.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The skill instructs use of Tavily, DeepSeek, and OpenRouter but does not warn that user prompts, search terms, generated summaries, and possibly output-related content will be transmitted to external providers. This lack of disclosure creates a privacy and data-handling risk, especially if users include sensitive material in requests or if generated content reflects user context.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The system prompt requires a Chinese translation and Chinese background text, and later rendering logic and branding also assume Chinese content. This forces a specific locale/language behavior for all users without opt-in or documented regional restriction, which violates the language-choice policy.

Content

No source excerpt is available for this finding.

Dynamic import via __import__()

Medium
Category
Dangerous Code Execution
Confidence
75% confidence
Finding

Dynamic import() can load arbitrary modules at runtime, bypassing static analysis and potentially importing malicious code.

Content

Scanner excerpt · scripts/generate_cards.py (reported line 59)May include surrounding context.

python
missing = []
    for mod, pkg in required.items():
        try:
            __import__(mod)
        except ImportError:
            missing.append(pkg)
    if missing:

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
93% confidence
Finding

The script automatically installs missing Python packages at runtime by invoking pip through subprocess. This is dangerous because it causes code from external package indexes to be fetched and executed without prior approval, and in an agent/automation context it can expand the trust boundary to the network and local package configuration, enabling dependency confusion or malicious package execution.

Content

Scanner excerpt · scripts/generate_cards.py (reported line 64)May include surrounding context.

python
missing.append(pkg)
    if missing:
        print(f"[setup] Installing missing packages: {', '.join(missing)}")
        subprocess.check_call(
            [sys.executable, "-m", "pip", "install", "-q"] + missing
        )

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
97% confidence
Finding

The dependency is specified with a lower bound only, so builds may resolve to different versions over time. This weakens reproducibility and can unintentionally introduce vulnerable or incompatible releases through normal dependency resolution or supply-chain drift.

Content

Scanner excerpt · requirements.txt (reported line 1)May include surrounding context.

text
tavily-python>=0.5.0
openai>=1.0.0
Pillow>=10.0.0
requests>=2.28.0

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
97% confidence
Finding

Using an unpinned version for the OpenAI package means the installed release is not deterministic across environments or future installs. This increases supply-chain risk and can cause the skill to pick up breaking or vulnerable versions without code changes.

Content

Scanner excerpt · requirements.txt (reported line 2)May include surrounding context.

text
tavily-python>=0.5.0
openai>=1.0.0
Pillow>=10.0.0
requests>=2.28.0

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
99% confidence
Finding

Pillow is unpinned despite having a history of security advisories, so the environment may resolve to an affected release without visibility. Because image parsing libraries can expose denial-of-service or code-execution attack surface when processing untrusted files, version drift here is more concerning than for a purely utility package.

Content

Scanner excerpt · requirements.txt (reported line 3)May include surrounding context.

text
tavily-python>=0.5.0
openai>=1.0.0
Pillow>=10.0.0
requests>=2.28.0

Unverifiable Dependency: Pillow has 16 known advisory(ies) (CVE-2016-2533 (Pillow buffer overflow in ImagingPcdDecode); CVE-2023-50447 (Arbitrary Code Execution in Pillow); CVE-2021-27922 (Pillow Uncontrolled Resource Consumption) +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
95% confidence
Finding

The manifest references Pillow without pinning to a specific release, and Pillow has multiple known historical advisories including resource-exhaustion and potential code-execution issues in some versions. Because the actual installed version is unconstrained beyond a minimum, it is not possible to verify that deployments avoid affected releases.

Content

No source excerpt is available for this finding.

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
99% confidence
Finding

Requests is unpinned, so future installs can silently pull in versions with known security defects or behavior changes. As an HTTP client commonly used with untrusted URLs and remote services, dependency drift can expose the skill to credential leakage, TLS-related issues, or other transport-layer weaknesses present in affected releases.

Content

Scanner excerpt · requirements.txt (reported line 4)May include surrounding context.

text
tavily-python>=0.5.0
openai>=1.0.0
Pillow>=10.0.0
requests>=2.28.0

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
95% confidence
Finding

The requests package is not pinned, and the package has known advisories in certain versions, including credential leakage and transport-related flaws. Without an exact version or lockfile, the deployed environment may resolve to an affected release, making the security posture unverifiable.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
82% confidence
Finding

At L0982 the comment says the comic area has words 'already integrated by AI', implying the final image reliably includes the required vocabulary. However, the code only loads and crops the generated image and never calls the defined _overlay_word_bubbles helper (L1235-L1269) or otherwise validates that the words are actually present. This is an intent/documentation mismatch in inline comments rather than a static bug report.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.