Back to skill

Security audit

diy-pc-ingest

Security checks across malware telemetry and agentic risk

Overview

The skill is mostly transparent about updating Notion PC-inventory tables, but it should be reviewed because it runs an unpinned dependency with the full environment and ships a legacy script with broader page-mutation behavior.

Install only if you are comfortable granting this skill write/archive access to the specified Notion tables. Use a Notion integration shared only with the DIY_PC targets, avoid running it in an environment containing unrelated secrets, verify or pin the notion-api-automation dependency if possible, review plan output before --apply, and do not use the deprecated Python script for real writes.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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)

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/notion_apply_records.js:84
Finding
Complete Host Environment Exposed to an External Dependency Process<![CDATA[ ## Vulnerability Details **File Location**: `scripts/notion_apply_records.js`, lines 84–85 and 101–104 **Vulnerability Type**: Excessive environment-variable disclosure to a child process **Risk Level**: Medium ### Vulnerable Code ```javascript function safeEnv(extra = {}) { return { ...process.env, ...extra }; } ``` ```javascript const env = safeEnv({ NOTION_VERSION: notionVersion() }); let out = ''; try { out = execFileSync('node', args, { encoding: 'utf-8', env, stdio: ['ignore', 'pipe', 'pipe'] }).trim(); } ``` ### Technical Analysis The script copies the complete parent-process environment into the child process that executes `notionctl.mjs`. This includes not only the Notion credential needed for the operation, but potentially unrelated cloud credentials, database passwords, CI/CD tokens, signing keys, proxy credentials, and other secrets injected into the host agent. The child component is installed separately and is outside this project's reviewed source. Providing it with all available environment variables violates least privilege: the declared functionality requires Notion authentication and limited runtime configuration, not access to every environment-backed secret. Although environment inheritance is common for child processes, it creates a security boundary problem when the child is an independently distributed dependency. The exposure is particularly significant in combination with the unpinned dependency identified separately in this report. ### Attack Path 1. An attacker compromises the installed `notion-api-automation` dependency, its distribution account, or a mutable release. 2. The user invokes `scripts/notion_apply_records.js`. 3. The script resolves and executes the dependency's `notionctl.mjs`. 4. `safeEnv()` passes the complete `process.env` object to that process. 5. The compromised dependency enumerates environment variables and retrieves unrelated credentials. 6. The dependency transmits or other ...[truncated 576 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Replace full environment inheritance with an explicit allowlist. Include only variables required for process execution and the documented Notion workflow, for example: - `PATH` - `HOME`, if module resolution requires it - `NOTION_API_KEY` - Explicitly supported legacy Notion authentication variables, only if necessary - `NOTION_VERSION` - Deliberately supported TLS or proxy variables Example hardening approach: ```javascript function notionctlEnv() { const allowed = [ 'PATH', 'HOME', 'NOTION_API_KEY', 'NOTION_TOKEN', 'NOTION_API_TOKEN', 'HTTPS_PROXY', 'NO_PROXY', 'NODE_EXTRA_CA_CERTS' ]; const env = {}; for (const key of allowed) { if (process.env[key] !== undefined) { env[key] = process.env[key]; } } env.NOTION_VERSION = notionVersion(); return env; } ``` Use this restricted object as the `env` option for `execFileSync`. Document each forwarded variable and remove legacy authentication aliases when compatibility is no longer necessary. Add an automated test confirming that an unrelated sentinel secret in `process.env` is not inherited by the child process. ]]>

T08 · Insecure Dependencies

Warning
Location
SKILL.md:13
Finding
Security-Critical Executable Dependency Is Installed Without an Immutable Version<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md`, lines 13–17 **Additional Location**: `README.md`, lines 22–27 **Vulnerability Type**: Unpinned third-party executable dependency **Risk Level**: Medium ### Vulnerable Code ```markdown Install the required dependency skill via ClawHub before using this skill: ```bash clawhub install notion-api-automation ``` ``` The README repeats the mutable installation instruction: ```markdown - Recommended installation: `clawhub install notion-api-automation` - `scripts/notion_apply_records.js` uses the dependency skill's `notionctl.mjs api`. ``` ### Technical Analysis The installation command identifies the dependency only by package name. It does not specify an immutable version, commit, checksum, or content digest. The primary script subsequently locates and executes `notion-api-automation/scripts/notionctl.mjs`, making that dependency part of the Skill's trusted execution path. Because the installed content can change independently after this project has been audited, a future compromised, malicious, or incompatible release could execute with the Skill process privileges. The dependency also receives the Notion credential and, under the current implementation, the complete parent environment. No evidence was found that the dependency is currently malicious. The finding concerns the absence of integrity and version controls around a security-critical executable component. ### Attack Path 1. An attacker compromises the dependency publisher, registry entry, release process, or distribution infrastructure. 2. The attacker publishes modified content under the existing `notion-api-automation` package name. 3. A user follows the documented unversioned installation command. 4. The Skill resolves the installed `notionctl.mjs` and executes it with Node.js. 5. The modified dependency gains the permissions and credentials available to the Skill process. 6. It can misuse the Notion credential, process pri ...[truncated 578 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Pin `notion-api-automation` to a reviewed immutable version or content digest. - Record the expected publisher identity and package source. - Verify package integrity before executing `notionctl.mjs`. - Maintain a compatibility matrix for the required `api --compact --method --path --body-json` interface. - Review dependency updates before changing the pin. - Combine dependency pinning with the environment allowlist recommended in the first finding. - If ClawHub does not support immutable version syntax, vendor a reviewed dependency version or add a local digest verification step before execution. - Fail closed when the resolved dependency does not match the expected version or digest. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/_deprecated/notion_apply_records.py:449
Finding
Deprecated Script Can Modify or Archive Pages Outside the Configured Notion Target<![CDATA[ ## Vulnerability Details **File Location**: `scripts/_deprecated/notion_apply_records.py`, lines 449–465 **Vulnerability Type**: Missing authorization boundary validation for direct page operations **Risk Level**: Medium ### Vulnerable Code ```python # Escape hatch: update a specific page by id (used for manual cleanup / de-dup). if page_id: existing_page = req("GET", f"/pages/{page_id}") patch = build_patch( schema, props_in, existing_page.get("properties") or {}, overwrite ) body = {} if patch: body["properties"] = patch if archive: body["archived"] = True if body: updated = req("PATCH", f"/pages/{page_id}", body) summary["updated"] += 1 outputs.append({ "action": "updated", "target": target, "id": updated.get("id"), "url": updated.get("url") }) ``` ### Technical Analysis When a record contains `page_id` or `id`, the deprecated Python implementation fetches that page and patches or archives it without checking whether its parent database or data source matches the configured target. The selected `target` controls which schema is loaded, but it does not authorize the supplied page identifier. Consequently, the actual access boundary becomes the entire set of pages visible to the Notion integration rather than the four configured DIY_PC targets. The primary JavaScript implementation contains the necessary parent validation in `scripts/notion_apply_records.js` lines 294–297, but the legacy Python file remains executable and includes a Python entry point. Labeling it deprecated does not technically prevent direct invocation. ### Attack Path 1. An attacker or untrusted input source obtains the ID of a page accessible to the configured Notion integration but outside the selected DIY_PC target. 2. The attacker supplies a JSONL record containing that `page_id`. 3. The record includes compatible pr ...[truncated 908 chars]
Remediation
<![CDATA[ ## Remediation Suggestions The preferred fix is to remove the deprecated executable from the distributed Skill if it is no longer supported. If it must remain, port the JavaScript implementation's target-parent validation before allowing any direct update or archive: 1. Fetch the supplied page. 2. Read `existing_page["parent"]`. 3. Normalize UUID formatting before comparison. 4. Require the parent `data_source_id` or legacy `database_id` to match the configured target. 5. Reject the operation before constructing or sending a patch when neither identifier matches. 6. Fail closed if the parent is absent or has an unexpected type. Conceptual validation: ```python def normalize_id(value): return (value or "").replace("-", "") parent = existing_page.get("parent") or {} same_data_source = ( normalize_id(parent.get("data_source_id")) == normalize_id(IDS[target]["data_source_id"]) ) same_database = ( normalize_id(parent.get("database_id")) == normalize_id(IDS[target]["database_id"]) ) if not (same_data_source or same_database): raise RuntimeError("Page does not belong to configured target") ``` Also remove executable permissions or replace the file with non-executable historical documentation. Add tests proving that a page from another accessible database cannot be updated or archived. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Rogue AgentSelf-Modification, Session Persistence
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (13)

Lp3

Medium
Category
MCP Least Privilege
Confidence
70% confidence
Finding
Without declared permissions the skill's intent is opaque and cannot be validated.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
This is a mismatch because the description emphasizes ingesting pasted receipts/specs with classification, enrichment, and follow-up, while the code explicitly says it does not parse raw text and only applies already-structured JSONL records to Notion. Its primary behavior is a deterministic Notion record planner/upserter. In addition, the code includes capabilities not mentioned in the description: archiving rows, updating explicit page IDs, dry-run planning, and mirroring storage items into another table. Access to Notion is consistent with the general domain, but the described high-level functionality does not accurately represent what the code itself actually does.

Description-Behavior Mismatch

Medium
Confidence
89% confidence
Finding
The manifest describes ingesting receipts/specs into Notion tables with enrichment, follow-up, and upsert, which reasonably covers create/update behavior. However, this script also accepts arbitrary page IDs and can archive pages directly, enabling manual cleanup/de-dup style record management that goes beyond straightforward ingest of pasted PC-part data.

Description-Behavior Mismatch

Medium
Confidence
92% confidence
Finding
The manifest says the skill ingests receipts/specs into DIY_PC Notion tables, which implies writing normalized records to the requested destination. This code can additionally synthesize and write a second derived record into the pcconfig table when ingesting storage items, an extra side effect not conveyed by the manifest description.

Natural-Language Policy Violations

Low
Confidence
86% confidence
Finding
The README explicitly instructs that Japanese property names must be used as-is, which imposes a specific language convention. Because the file does not present this as an optional or user-selected locale, it can be read as a language-policy constraint without opt-in.

Natural-Language Policy Violations

Medium
Confidence
87% confidence
Finding
The instruction 'Prefer Japanese column names as they exist in each table' imposes a specific language/locale in the skill behavior. The policy allows locale constraints only when the skill offers a language choice or clearly documents a justified region-specific requirement, which is not stated here.

Natural-Language Policy Violations

Low
Confidence
91% confidence
Finding
This JSON example includes several hard-coded Japanese field names such as "名前", "型番", "シリアル", and "取り外し表示名" alongside English names, but the file provides no explanation that the skill is intended for Japanese-language Notion schemas or that locale is configurable. That can violate language/locale policy by implicitly requiring a specific language without user opt-in or documented justification.

Natural-Language Policy Violations

Low
Confidence
89% confidence
Finding
The default configuration and later record-processing logic hard-code Japanese property names such as "名前", "型番", "シリアル", and "取り外し表示名". This creates a locale-specific behavior in the skill without any visible opt-in or documented language-selection mechanism in the file.

External Transmission

Medium
Category
Data Exfiltration
Content
import urllib.request
from dataclasses import dataclass

API = "https://api.notion.com/v1"

DEFAULT_NOTION_VERSION = "2025-09-03"
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Env Variable Harvesting

High
Category
Data Exfiltration
Content
def notion_api_key(cfg: dict) -> str:
    # Prefer env
    tok = os.environ.get("NOTION_API_KEY") or os.environ.get("NOTION_TOKEN")
    if tok:
        return tok.strip()
Confidence
70% confidence
Finding
Code accesses environment variables that may contain secrets (API keys, tokens). This is a common pattern for credential theft.

Env Variable Harvesting

High
Category
Data Exfiltration
Content
def notion_api_key(cfg: dict) -> str:
    # Prefer env
    tok = os.environ.get("NOTION_API_KEY") or os.environ.get("NOTION_TOKEN")
    if tok:
        return tok.strip()
Confidence
70% confidence
Finding
Code accesses environment variables that may contain secrets (API keys, tokens). This is a common pattern for credential theft.

Env Variable Harvesting

High
Category
Data Exfiltration
Content
return tok

    # Legacy local file path (author setup)
    key_path = os.environ.get("NOTION_API_KEY_FILE") or "~/.config/notion/api_key"
    return open(os.path.expanduser(key_path), "r", encoding="utf-8").read().strip()
Confidence
70% confidence
Finding
Code accesses environment variables that may contain secrets (API keys, tokens). This is a common pattern for credential theft.

Session Persistence

Medium
Category
Rogue Agent
Content
Notion API:
- Query/schema via data_sources
- Create rows via pages with parent.database_id
"""

from __future__ import annotations
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.

VirusTotal

VirusTotal findings are pending for this skill version.

View on VirusTotal

Static analysis

Detected: suspicious.dangerous_exec

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/notion_apply_records.js:103