Back to skill

Security audit

Notion Diary

Security checks for vulnerabilities and agentic risk

Overview

This Notion diary skill is mostly coherent, but its photo upload path can read and upload any local file path supplied as an image, and updates replace existing Notion page content without an explicit confirmation.

Review this before installing if you will use photo sync. Only pass image files you explicitly chose, and avoid letting copied diary text or prompts supply file paths. Be aware that syncing an existing date can replace the current Notion page body.

Vulnerability Patterns
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (1)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/notion_diary_sync.py:459
Finding

Arbitrary Local File Disclosure Through Unrestricted Image Upload

Content
View full analysis

Vulnerability Details

File Location: scripts/notion_diary_sync.py, lines 236–257 and 459–466
Vulnerability Type: Unrestricted local file read and network upload
Risk Level: High

Vulnerable Code

python
def upload_small_file(self, path: pathlib.Path) -> str:
    if not path.exists():
        raise NotionSyncError(f"Image file not found: {path}")
    size = path.stat().st_size
    if size > MAX_IMAGE_BYTES:
        raise NotionSyncError(
            f"Image file is larger than 20 MB and cannot use single-part upload: {path}"
        )
    mime = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
    create_payload = {
        "mode": "single_part",
        "filename": path.name,
        "content_type": mime,
    }
    created = self.request("POST", "/file_uploads", payload=create_payload)
    file_upload_id = created["id"]

    boundary = f"----notiondiary{uuid.uuid4().hex}"
    file_bytes = path.read_bytes()
python
def resolve_image_reference(
    client: NotionClient,
    image_ref: str,
    strict_images: bool,
) -> ResolvedImage:
    try:
        if re.match(r"^https?://", image_ref):
            parsed = urllib.parse.urlparse(image_ref)
            name = pathlib.Path(parsed.path).name or "image"
            return ResolvedImage(source=image_ref, name=name, external_url=image_ref)
        path = pathlib.Path(image_ref).expanduser().resolve()
        upload_id = client.upload_small_file(path)
        return ResolvedImage(source=str(path), name=path.name, file_upload_id=upload_id)

Technical Analysis

The --image argument is treated as an arbitrary local filesystem path. The implementation expands and resolves that path, then reads the referenced file using path.read_bytes() and transmits its contents to the Notion file-upload API.

The validation is insufficient for the declared photo-synchronization feature:

  • It ch ...[truncated 2907 chars]
Remediation
View remediation

Remediation Suggestions

  1. Restrict upload roots

    • Accept local files only from host-managed attachment directories.
    • Resolve both the candidate path and approved roots, then verify that the candidate remains beneath an approved root.
    • Reject paths outside those directories.
  2. Reject unsafe filesystem objects

    • Require path.is_file().
    • Reject symbolic links before opening the target.
    • Open files using safeguards that prevent symlink races where supported.
    • Revalidate the opened file descriptor before reading.
  3. Verify actual image content

    • Allow only explicitly supported image formats.
    • Validate magic bytes rather than relying on filename extensions or mimetypes.guess_type().
    • Decode the file with a trusted image parser and reject content that cannot be decoded as an image.
    • Derive the upload MIME type from verified content.
  4. Require explicit authorization

    • Ensure every local image originates from a user-provided attachment or an explicitly approved path.
    • Require confirmation before accessing paths outside the host’s attachment directory.
    • Do not allow diary text or retrieved conversation content to introduce local file paths automatically.
  5. Minimize path disclosure

    • Do not place absolute local paths or detailed exception messages in Notion fallback paragraphs.
    • Replace them with a generic message such as Image upload failed.
    • Keep detailed diagnostics local and redact home-directory and credential-related paths.
  6. Add security tests

    • Verify rejection of files such as SSH keys, environment files, and arbitrary text documents.
    • Test path traversal, home-directory expansion, symbolic links, misleading image extensions, malformed images, and files outside approved roots.
    • Confirm that valid user attachments continue to upload successfully.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (6)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding

The skill explicitly relies on environment access for NOTION_API_KEY, file reads for referenced resources and content files, and network access to sync data to Notion, but it does not declare any tool scope such as permissions or allowed-tools. That mismatch weakens policy enforcement and reviewability: a runtime may grant broader capabilities than users or reviewers expect, increasing the risk of secret exposure or unintended external data transmission.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

This markdown file states that the script uploads local files using Notion's direct upload flow, which transmits user data to an external service. Under the markdown-file criteria for missing user warnings, there is no accompanying warning about privacy, data transfer, or the effect on user content.

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

python
from dataclasses import dataclass
from typing import Dict, Iterable, List, Optional, Sequence, Tuple

API_BASE = "https://api.notion.com/v1"
API_VERSION = "2026-03-11"
MAX_TEXT_CHUNK = 1900
MAX_IMAGE_BYTES = 20 * 1024 * 1024

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

When syncing an existing entry, the script unconditionally deletes all current child blocks before appending rebuilt content. Although updating a diary entry is part of the skill's purpose, this irreversible content replacement is not disclosed to the user via a confirmation prompt or explicit warning in the script output/docstring.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
87% confidence
Finding

The Style field is documented with only Chinese language options, which imposes a specific language/locale in the skill's natural-language interface. There is no indication that users may choose another language or that the Chinese-only labels are required for a region-specific purpose.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
86% confidence
Finding

The script embeds Chinese locale-specific labels such as style names and uses Chinese text in generated content and placeholders, which imposes a language choice on users without opt-in. This can violate language/locale policy when the skill is otherwise general-purpose and not documented as Chinese-specific.

Content

No source excerpt is available for this finding.