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## 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
## 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)

Lp3

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.

Missing User Warnings

Medium
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.

External Transmission

Medium
Category
Data Exfiltration
Content
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
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Missing User Warnings

Medium
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.

Natural-Language Policy Violations

Low
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.

Natural-Language Policy Violations

Low
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.

Static analysis

No suspicious patterns detected.