Back to skill

Security audit

Notion

Security checks for vulnerabilities and agentic risk

Overview

This is mostly a coherent Notion helper, but its profile-selection code can read an unintended local key file and send that value to Notion as an API token.

Review or patch the profile-selection snippet before installing. Use NOTION_API_KEY directly or restrict NOTION_PROFILE to known names such as personal/work, store tokens in a secret manager where possible, and share the Notion integration only with the pages and databases needed for the task.

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

Warning
Location
SKILL.md:54
Finding

Path Traversal Through Unvalidated Notion Profile Name

Content
View full analysis

Vulnerability Details

File Location: SKILL.md, lines 54-66 and 76-81
Vulnerability Type: Path traversal leading to unintended local-file disclosure
Risk Level: Medium

The Skill constructs a credential-file path using the unvalidated NOTION_PROFILE environment variable:

bash
mkdir -p ~/.config/notion
echo "ntn_personal_key" > ~/.config/notion/personal.key
echo "ntn_work_key"     > ~/.config/notion/work.key
chmod 600 ~/.config/notion/*.key
bash
NOTION_PROFILE="${NOTION_PROFILE:-personal}"
NOTION_KEY="${NOTION_API_KEY:-$(cat ~/.config/notion/${NOTION_PROFILE}.key 2>/dev/null)}"
[ -n "$NOTION_KEY" ] || echo "No Notion key for profile '$NOTION_PROFILE'"

The resulting file contents are subsequently included in an outbound HTTP Authorization header:

bash
notion() {
  local method="$1" path="$2"; shift 2
  curl -sS -X "$method" "https://api.notion.com/v1${path}" \
    -H "Authorization: Bearer $NOTION_KEY" \
    -H "Notion-Version: 2025-09-03" \
    -H "Content-Type: application/json" "$@"
}

Technical Analysis

NOTION_PROFILE is directly interpolated into the path ~/.config/notion/${NOTION_PROFILE}.key without an allowlist, character validation, or canonical-path containment check. A value containing ../ components can escape the intended Notion configuration directory.

The fixed .key suffix restricts the immediately reachable targets to paths whose resulting names end in .key, but it does not guarantee that the selected file is a configured Notion profile or that it remains under ~/.config/notion. If NOTION_API_KEY is unset, the contents of the selected file are assigned to NOTION_KEY.

The notion helper then sends NOTION_KEY to the official Notion API over HTTPS as a bearer token. Sending a valid Notion token to this endpoint is necessary for the declared functionality. However, allowing an unvalidated profile selector to source arb ...[truncated 1653 chars]

Remediation
View remediation

Remediation Suggestions

Restrict profile selection to explicitly supported names:

bash
NOTION_PROFILE="${NOTION_PROFILE:-personal}"

case "$NOTION_PROFILE" in
  personal|work) ;;
  *)
    echo "Invalid Notion profile" >&2
    return 1
    ;;
esac

key_file="$HOME/.config/notion/$NOTION_PROFILE.key"
NOTION_KEY="${NOTION_API_KEY:-$(cat "$key_file" 2>/dev/null)}"

if [ -z "$NOTION_KEY" ]; then
  echo "No Notion key for profile '$NOTION_PROFILE'" >&2
  return 1
fi

If arbitrary user-defined profile names are required:

  1. Permit only a narrow format such as ^[A-Za-z0-9_-]+$.
  2. Reject profile names containing path separators, traversal components, whitespace, or shell metacharacters.
  3. Resolve the key file to a canonical path and verify that it remains beneath $HOME/.config/notion/.
  4. Require the key file to be a regular file owned by the expected user and not writable by other users.
  5. Validate that the loaded value has an expected Notion token prefix, such as ntn_ or the documented legacy secret_, before using it as an Authorization header.
  6. Terminate immediately when the profile is invalid or the token is absent rather than merely printing an error and permitting later requests.
  7. Continue using HTTPS and avoid printing, logging, or embedding the token in command diagnostics.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Rogue AgentSelf-Modification, Session Persistence
Findings (8)

Tool Parameter Abuse

High
Category
Tool Misuse
Confidence
83% confidence
Finding

The skill includes direct guidance for deleting blocks, which is a destructive operation against user content. Although it says to confirm first and delete only identified blocks, exposing raw deletion primitives in an agent skill raises the risk of parameter misuse, targeting the wrong block IDs, or carrying out irreversible content loss in a high-value notes workspace.

Content

Scanner excerpt · SKILL.md (reported line 149)May include surrounding context.

notion PATCH /pages/{page_id} -d '{"properties": {"Status": {"select": {"name": "Done"}}}}'

text

To replace body content, delete the specific blocks (`DELETE /blocks/{block_id}`) and append new ones. Confirm with the user first, and delete only the blocks you identified by reading them, never the whole page's children as a batch.

## Databases

Session Persistence

Medium
Category
Rogue Agent
Confidence
60% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · README.md (reported line 9)May include surrounding context.

md
## Quick Start

1. Create a Notion integration at <https://www.notion.so/my-integrations>
2. Copy the Internal Integration Token (starts with `ntn_`)
3. Export it: `export NOTION_API_KEY=ntn_xxx`
4. Share the target pages and databases with the integration in the Notion UI, otherwise they are invisible to the API

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The activation text is unusually broad: it triggers not only on explicit Notion requests but also on generic requests to 'log' or 'file something' into a workspace, even if the user never mentions the API. That increases the chance this skill is invoked in ambiguous contexts and performs external writes to a real Notion workspace when a narrower, safer skill or a clarification step would be more appropriate.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
77% confidence
Finding

The skill encourages storing long-lived API tokens in environment variables and plaintext profile files under ~/.config/notion. While common, this creates session persistence for a high-value credential and increases exposure to local compromise, shell history mishandling, accidental backups, or misuse by other processes running as the same user.

Content

Scanner excerpt · SKILL.md (reported line 35)May include surrounding context.

Authentication

Create an integration at https://www.notion.so/my-integrations and copy the Internal Integration Token. Current tokens start with ntn_; older ones start with secret_ and still work.

bash
export NOTION_API_KEY=ntn_your_key_here

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
80% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · SKILL.md (reported line 51)May include surrounding context.

mkdir -p ~/.config/notion echo "ntn_personal_key" > ~/.config/notion/personal.key echo "ntn_work_key" > ~/.config/notion/work.key chmod 600 ~/.config/notion/*.key

text

Resolve the active key like this. An explicit `NOTION_API_KEY` always wins; otherwise the profile file is used, defaulting to `personal`:

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 · SKILL.md (reported line 71)May include surrounding context.

bash
notion() {
  local method="$1" path="$2"; shift 2
  curl -sS -X "$method" "https://api.notion.com/v1${path}" \
    -H "Authorization: Bearer $NOTION_KEY" \
    -H "Notion-Version: 2025-09-03" \
    -H "Content-Type: application/json" "$@"

External Transmission

Medium
Category
Data Exfiltration
Confidence
50% 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 · SKILL.md (reported line 71)May include surrounding context.

bash
notion() {
  local method="$1" path="$2"; shift 2
  curl -sS -X "$method" "https://api.notion.com/v1${path}" \
    -H "Authorization: Bearer $NOTION_KEY" \
    -H "Notion-Version: 2025-09-03" \
    -H "Content-Type: application/json" "$@"

Autonomous Decision Making

Medium
Category
Excessive Agency
Confidence
75% confidence
Finding

Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Content

Scanner excerpt · SKILL.md (reported line 203)May include surrounding context.

}'

text

Prefer additive changes. If the user wants a property retyped, suggest adding a new one and migrating rather than converting in place, because a type change discards values that do not fit the new type. Setting a property to `null` removes it; never do that without confirmation.

Note that the API cannot change view-level filters or sorts. Those are UI-only. If the user asks for that, say so instead of changing the underlying schema as a substitute.

Static analysis

No suspicious patterns detected.