Back to skill

Security audit

Notion Content Pipeline

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly does the Notion sync it advertises, but it also has under-scoped automation that can overwrite content, mutate Notion records, and execute a sibling fact-checker script outside the package.

Install only if you are comfortable giving it a Notion integration token that can create, edit, and archive pages in the shared workspace. Configure the parent page, sync-map path, and pipeline database ID explicitly, keep the sync map protected, avoid relying on dry-run as a no-execution mode, and review backups before using pull, push overwrite, or pipeline advance. The sibling fact-checker execution should be removed or pinned before routine use.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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
Findings (3)

T08 · Insecure Dependencies

Error
Location
scripts/pipeline_advance.py:223
Finding

Unverified External Script Execution, Including During Dry-Run

Content
View full analysis
/scripts → workspace FACT_CHECKER_SCRIPT = _WORKSPACE / "skills" / "fact-checker" / "scripts" / "fact_check.py" PYTHON = "/Users/loki/.pyenv/versions/3.14.3/bin/python3" def run_factcheck(file_path: Path) -> Optional[str]: """Run fact_check.py if available. Returns report string or None.""" if not FACT_CHECKER_SCRIPT.exists(): return None result = subprocess.run( [PYTHON, str(FACT_CHECKER_SCRIPT), str(file_path)], capture_output=True, text=True, timeout=120, ) output = result.stdout if result.stderr: output += f"\n[stderr]: {result.stderr[:200]}" return output.strip() if output.strip() else "(no output)" # The subprocess is reached even when dry_run is True. report = run_factcheck(file_path) ``` ### Technical Analysis The Skill invokes a sibling `fact_check.py` script that is outside its own audited package. The only validation is an existence check. It does not verify the external file's ownership, permissions, expected version, cryptographic digest, or provenance. The Python interpreter is also selected through a hardcoded absolute path outside the Skill's trust boundary. If either that interpreter or the sibling script can be replaced or modified, invoking the pipeline executes attacker-controlled code with the privileges of the user running the Skill. The fact-check call is not conditioned on `dry_run`. Consequently, `--dry-run`, which is documented as a preview that should not make changes, can still execute arbitrary external code and allow any network or filesystem side effects implemented by that external c ...[truncated 1622 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/notion_content_sync.py:296
Finding

Existing Notion Page Is Archived Before Replacement Is Successfully Created

Content
View full analysis
dict: if sync_map_path.exists(): return json.loads(sync_map_path.read_text()) return {} ``` ### Technical Analysis The overwrite sequence archives the currently mapped Notion page before creating and fully populating its replacement. The operations are not transactional. Any network failure, API validation error, timeout, process termination, or block-append failure after archival can leave the existing page archived without a valid replacement. Additionally, `existing_id` is loaded directly from the local sync map and is used in a state-changing API request without checking that: - It is a valid Notion page identifier. - It represents the expected page. - It belongs to the configured parent page. - It was originally created or managed by this Skill. A user or process able to modify the sync-map file can redirect the archival operation to another page accessible to the Notion integration. ### Attack Path #### Availability failure path 1. A Markdown file is already associated with an existing Notion page. 2. The user invokes `push` with the default overwrite behavior. 3. The Skill archives the existing page. 4. Creation of the replacement fails because of a con ...[truncated 1212 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/pipeline_advance.py:57
Finding

Conflicting Hardcoded Database Fallback Can Modify an Unintended Notion Pipeline

Content
View full analysis
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (21)

Tainted flow: 'key' from os.environ.get (line 35, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/create_pipeline_db.py (reported line 101)May include surrounding context.

python
"Tags": {"rich_text": {}},
        },
    }
    r = requests.post(f"{NOTION_API}/databases", headers=headers(key), json=payload)
    if r.status_code != 200:
        print(f"ERROR creating database: {r.status_code}: {r.text[:300]}", file=sys.stderr)
        sys.exit(1)

Tainted flow: 'key' from os.environ.get (line 35, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/create_pipeline_db.py (reported line 127)May include surrounding context.

python
"Tags": {"rich_text": [{"text": {"content": tags}}]},
        },
    }
    r = requests.post(f"{NOTION_API}/pages", headers=headers(key), json=payload)
    if r.status_code != 200:
        print(f"  ERROR adding '{title[:40]}': {r.status_code}: {r.text[:200]}", file=sys.stderr)
        return None

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The declared description presents a broader bidirectional markdown/Notion synchronization skill with local file operations and mapping management. The supplied code chunk is much narrower: it creates a Notion database with specific properties and optionally seeds records from a JSON file. While pipeline database creation is one part of the declared purpose, the primary sync behaviors described are absent from this code. There is no undeclared suspicious capability beyond Notion database/page creation and local reading of an optional seed JSON file, but the description does not accurately represent what this code chunk actually does, so this is a mismatch.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding

The implemented core sync behavior largely matches the declared Markdown↔Notion push/pull functionality, including per-file push/pull, batch push-all, and tracking mappings. However, the description also claims support for managing a content pipeline database and creating a pipeline DB with specific properties and statuses. None of that exists in the provided code. The code only creates ordinary Notion pages under a parent page, sets a title property, appends content blocks, pulls page blocks back to Markdown, lists tracked mappings, and archives old pages on overwrite. Because a significant declared capability—pipeline database creation/management—is absent, the description does not accurately represent the full behavior/capabilities of the code.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding

The skill advertises capabilities that include environment access, file read/write, shell execution, and outbound network use, but it does not declare any explicit tool scope such as allowed-tools or permissions. That omission weakens least-privilege controls and makes it easier for an agent or operator to invoke the skill with broader access than users may expect, especially given it handles local files and a live API token.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The documentation exposes a live 1Password secret retrieval path and instructs operators to populate an API key from it, without emphasizing handling restrictions. Even though the secret value is not embedded, publishing operational secret locations and normalizing direct retrieval increases the risk of credential misuse, accidental disclosure in logs, and unsafe copy/paste patterns in shared agent environments.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
82% confidence
Finding

The skill explicitly creates and updates persistent local state, including sync maps and derivative artifacts from pull/humanize/fact-check workflows, and also creates remote Notion pages/databases. Persistent state can become a security issue when retention, location, and lifecycle are not tightly controlled, because it may leak document metadata, page IDs, workflow history, or modified content across sessions.

Content

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

md
## Scripts

- `scripts/notion_content_sync.py` — push/pull individual files or all at once
- `scripts/create_pipeline_db.py` — create a Notion database for content pipeline tracking
- `scripts/pipeline_advance.py` — **full round-trip advance**: pull → humanize → fact-check → push → status update

## Configuration

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The skill says it is scoped to markdown↔Notion sync, but it also documents an automated workflow that modifies local content and invokes an external fact-checker script. This scope expansion increases the attack surface because a user expecting simple sync may instead trigger local file transformations and secondary script execution with broader side effects.

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 explains an overwrite workflow that archives an old page, creates a new page, and updates references, which can materially affect user data. The section provides operational steps but does not include any warning or caution that the process modifies existing Notion content and may disrupt links, history, or recovery expectations.

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

python
sys.exit(1)

NOTION_VERSION = "2022-06-28"
NOTION_API = "https://api.notion.com/v1"


def get_key() -> str:

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_content_sync.py (reported line 33)May include surrounding context.

python
sys.exit(1)

NOTION_VERSION = "2022-06-28"
NOTION_API = "https://api.notion.com/v1"


def get_key() -> str:

Tainted flow: 'payload' from requests.post (line 114, network input) → requests.post (network output)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · scripts/create_pipeline_db.py (reported line 101)May include surrounding context.

python
"Tags": {"rich_text": {}},
        },
    }
    r = requests.post(f"{NOTION_API}/databases", headers=headers(key), json=payload)
    if r.status_code != 200:
        print(f"ERROR creating database: {r.status_code}: {r.text[:300]}", file=sys.stderr)
        sys.exit(1)

Tainted flow: 'payload' from requests.post (line 114, network input) → requests.post (network output)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · scripts/create_pipeline_db.py (reported line 127)May include surrounding context.

python
"Tags": {"rich_text": [{"text": {"content": tags}}]},
        },
    }
    r = requests.post(f"{NOTION_API}/pages", headers=headers(key), json=payload)
    if r.status_code != 200:
        print(f"  ERROR adding '{title[:40]}': {r.status_code}: {r.text[:200]}", file=sys.stderr)
        return None

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The pull operation writes remote Notion content directly to the specified local file path without any confirmation, backup, or conflict detection. If the remote page was modified unexpectedly, mapped incorrectly, or influenced by another collaborator, local content can be silently destroyed or replaced, which is especially risky in a two-way sync workflow handling user-authored drafts.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

The script is a Notion content-pipeline tool but reaches into a sibling skill in the workspace and executes its script directly. This creates a cross-skill trust boundary violation: a compromised or modified fact-checker skill can run arbitrary code whenever this pipeline script is used, which is more dangerous in an agent-skill ecosystem where skills should be isolated and composable only through explicit, trusted interfaces.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/pipeline_advance.py (reported line 221)May include surrounding context.

python
if not FACT_CHECKER_SCRIPT.exists():
        return None

    result = subprocess.run(
        [PYTHON, str(FACT_CHECKER_SCRIPT), str(file_path)],
        capture_output=True,
        text=True,

Tainted flow: 'humanized_md' from pathlib.Path.read_text (line 349, file read) → pathlib.Path.write_text (file write)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · scripts/pipeline_advance.py (reported line 345)May include surrounding context.

python
diff_path = file_path.with_suffix(".humanizer.diff")
            if not dry_run:
                diff_path.write_text(diff_str, encoding="utf-8")
                file_path.write_text(humanized_md, encoding="utf-8")
            else:
                print(f"  [dry-run] Would write humanized text and diff → {diff_path}")
        else:

Description-Behavior Mismatch

Low
Category
Not specified by scanner
Confidence
78% confidence
Finding

The documentation frames the skill as scoped to the content pipeline DB and markdown sync, and explicitly says not to use it for arbitrary database reads. Yet it embeds and promotes use of both a Content Pipeline DB and a Tweet Pipeline DB, suggesting broader database interaction than the stated boundary.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Low
Category
Not specified by scanner
Confidence
95% confidence
Finding

The manifest describes a content pipeline with statuses "Seed → Draft → Review → Published", but the database created here defines "Seed", "Draft", "Humanized", "In Review", and "Published". This is a semantic mismatch between the advertised workflow and the actual schema the script provisions.

Content

No source excerpt is available for this finding.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · scripts/notion_content_sync.py (reported line 78)May include surrounding context.

python
def _req(method: str, url: str, key: str, **kwargs) -> dict:
    import requests
    resp = getattr(requests, method)(f"{NOTION_API}{url}", headers=_headers(key), **kwargs)
    if not resp.ok:
        print(f"Notion API error {resp.status_code}: {resp.text[:300]}", file=sys.stderr)
        resp.raise_for_status()

Context-Inappropriate Capability

Low
Category
Not specified by scanner
Confidence
79% confidence
Finding

The manifest focuses on two-way markdown↔Notion sync, page mapping, and pipeline database workflows. This script additionally modifies markdown content through a 'humanizer' pass and writes .humanizer.diff and .factcheck.txt artifacts, which are editorial transformation capabilities not clearly justified by the declared sync/pipeline purpose.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.