Back to skill

Security audit

GitHub Stars Export

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its stated GitHub-to-Notion sync purpose, but it needs Review because it can archive Notion data without confirmation and disables TLS checks while using the Notion token.

Install only if you are comfortable giving it GitHub CLI access and a Notion integration token. Use a Notion integration limited to a dedicated page or database, pass an explicit parent ID, review or fix the script before use, remove verify=False, and avoid rerunning sync against valuable databases until it has confirmation and dry-run safeguards.

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 (4)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/sync_stars_to_notion_db.py:91
Finding
TLS Certificate Verification Is Disabled for Authenticated Notion API Requests<![CDATA[ ## Vulnerability Details **File Location**: `scripts/sync_stars_to_notion_db.py`, lines 6-9, 91, 111, 122, and 157 **Vulnerability Type**: Improper certificate validation **Risk Level**: High ### Vulnerable Code ```python try: import requests import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) except ImportError: print("The 'requests' library is required. Install it using: pip install requests") sys.exit(1) ``` Every authenticated Notion request explicitly disables certificate validation: ```python response = requests.post(url, headers=HEADERS, json=payload, verify=False) ``` ```python response = requests.post(query_url, headers=HEADERS, json=payload, verify=False) ``` ```python requests.patch( patch_url, headers=HEADERS, json={"archived": True}, verify=False ) ``` ```python response = requests.post(url, headers=HEADERS, json=payload, verify=False) ``` ### Technical Analysis The script sends the `NOTION_API_KEY` in the HTTP `Authorization` header: ```python HEADERS = { "Authorization": f"Bearer {NOTION_TOKEN}", "Content-Type": "application/json", "Notion-Version": NOTION_VERSION } ``` Although the destination uses HTTPS, `verify=False` instructs `requests` not to authenticate the server certificate. Suppressing `InsecureRequestWarning` further conceals this unsafe condition from the user. An attacker who can intercept or redirect traffic can present an arbitrary TLS certificate and impersonate `api.notion.com`. This may be possible through a hostile network, compromised DNS, malicious proxy configuration, or an untrusted local root/proxy environment. The attacker could then obtain: - The Notion integration bearer token. - Repository names, owners, categories, URLs, and star counts. - Notion database and page identifiers. - API responses returned by the impersonated service. The same weakness applies to destructive `PATCH` requests used to archive database recor ...[truncated 1662 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove `verify=False` from every `requests.post` and `requests.patch` call. The default certificate validation behavior should be retained: ```python response = requests.post( url, headers=HEADERS, json=payload, timeout=30 ) ``` 2. Remove the global warning suppression: ```python urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) ``` 3. If a private enterprise CA is genuinely required, accept a user-configured CA bundle rather than disabling verification: ```python ca_bundle = os.environ.get("REQUESTS_CA_BUNDLE") response = requests.post( url, headers=HEADERS, json=payload, verify=ca_bundle if ca_bundle else True, timeout=30 ) ``` 4. Validate that `NOTION_API_KEY` is present before constructing or sending any request. 5. Add explicit connection and read timeouts to every request. 6. Rotate any Notion token that has already been used with this version of the script over an untrusted network. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/sync_stars_to_notion_db.py:97
Finding
Existing Notion Records Are Archived Before Input Is Validated<![CDATA[ ## Vulnerability Details **File Location**: `scripts/sync_stars_to_notion_db.py`, lines 97-126 and 165-183 **Vulnerability Type**: Unsafe destructive operation and improper operation ordering **Risk Level**: Medium ### Vulnerable Code The database-clearing function archives every record returned by the configured database: ```python def clear_database(db_id): """Archives all existing pages in the database so it can be 'overwritten'.""" print(f"Clearing existing entries in database {db_id}...") query_url = f"https://api.notion.com/v1/databases/{db_id}/query" has_more = True next_cursor = None count = 0 while has_more: payload = {} if next_cursor: payload["start_cursor"] = next_cursor response = requests.post(query_url, headers=HEADERS, json=payload, verify=False) if response.status_code != 200: print(f"Error querying database for clearing: {response.text}") break data = response.json() pages = data.get("results", []) for page in pages: page_id = page["id"] patch_url = f"https://api.notion.com/v1/pages/{page_id}" requests.patch(patch_url, headers=HEADERS, json={"archived": True}, verify=False) count += 1 has_more = data.get("has_more", False) next_cursor = data.get("next_cursor") print(f"Cleared {count} existing entries.") ``` The destructive operation occurs before the input file is parsed or checked for usable content: ```python config = load_config() db_id = config.get(args.db_name) if db_id: print(f"Found existing database ID in config for '{args.db_name}': {db_id}") clear_database(db_id) else: print(f"Creating new database '{args.db_name}'...") db_id = create_database(args.db_name, args.parent_id) config[args.db_name] = db_id save_config(config) print(f"Database cre ...[truncated 2611 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse and fully validate the input before making any Notion modifications: ```python rows = parse_markdown(args.input) if not rows: print("Refusing to clear the destination because the input contains no valid rows.") sys.exit(1) ``` 2. Verify the target database through the Notion API before modification. Confirm its ID, title, parent, and expected property schema. 3. Require explicit confirmation for replacement operations, with a separate `--yes` option for intentional automation. 4. Prefer non-destructive reconciliation: - Fetch existing records. - Upsert records using a stable repository identifier. - Archive only records that are proven to be obsolete. - Avoid clearing the complete database. 5. If full replacement is required, insert and validate the new records before archiving the old set, or synchronize into a staging database and swap only after success. 6. Check every archival response and stop immediately on failure: ```python response = requests.patch( patch_url, headers=HEADERS, json={"archived": True}, timeout=30 ) response.raise_for_status() ``` 7. Store additional target metadata in the configuration, not only the database ID, and reject mismatches. 8. Document recovery procedures and recommend using an integration restricted to the single intended database. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
scripts/sync_stars_to_notion_db.py:18
Finding
Opaque Hardcoded Notion Parent Page Is Used as the Default Data Destination<![CDATA[ ## Vulnerability Details **File Location**: `scripts/sync_stars_to_notion_db.py`, lines 18 and 164 **Vulnerability Type**: Unsafe default destination **Risk Level**: Low ### Vulnerable Code ```python DEFAULT_PARENT_PAGE_ID = "f94aa417-3269-4fa6-a869-dc5b22eb1cca" ``` The hardcoded value is automatically selected unless the user supplies a different parent: ```python parser.add_argument( "--parent-id", default=DEFAULT_PARENT_PAGE_ID, help="ID of the parent Notion page." ) ``` The value is then used to create the destination database: ```python db_id = create_database(args.db_name, args.parent_id) ``` ### Technical Analysis The Skill's declared behavior requires sending exported repository metadata to Notion. That network transmission is necessary for synchronization, and the code sends it only to the official `https://api.notion.com` endpoint. However, the destination page is an opaque, package-defined UUID rather than a value that the user must explicitly select. The project does not establish who owns this page or why it is an appropriate default for every installation. The Notion API normally prevents creation under a page that is not shared with the integration, reducing exploitability. Nevertheless, if the integration can access the hardcoded page—for example, because the Skill was developed for a shared workspace or the page remains shared—the script can create the database at a destination the user did not consciously select. This is an unsafe and unnecessary default. Least-privilege operation should require an explicit user-owned destination. ### Attack Path 1. The user configures a Notion integration token and runs the sync command without `--parent-id`. 2. The script automatically selects the hardcoded page UUID. 3. If that page is accessible to the integration, the script creates the database there. 4. The script uploads repository names, owners, URLs, categories, and star counts to that database. 5. Other users with ...[truncated 733 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove the hardcoded parent UUID. 2. Require either `--parent-id` or a clearly named environment variable: ```python parser.add_argument( "--parent-id", default=os.environ.get("NOTION_PARENT_PAGE_ID"), help="Explicit ID of the parent Notion page." ) if not args.parent_id: parser.error("--parent-id or NOTION_PARENT_PAGE_ID is required") ``` 3. Before uploading data, retrieve and display the selected parent page's title and workspace context, then request confirmation. 4. Document exactly what repository metadata will be transmitted and where it will be stored. 5. Recommend creating a dedicated Notion integration shared only with the intended parent page or database. ]]>

T08 · Insecure Dependencies

Note
Location
requirements.txt:1
Finding
Python Dependency Is Not Version-Pinned<![CDATA[ ## Vulnerability Details **File Location**: `requirements.txt`, line 1 **Vulnerability Type**: Unpinned third-party dependency **Risk Level**: Low ### Vulnerable Code ```text requests ``` ### Technical Analysis The project installs `requests` without a version constraint or hash. Consequently, each installation may retrieve a different release from the configured Python package index. The package name is legitimate and there is no evidence of typosquatting or a deliberately malicious dependency. However, the unbounded specification prevents reproducible installation and leaves the Skill exposed to: - A future compromised or malicious package release. - Unexpected behavior or incompatibility introduced by a later version. - Variation between audited and deployed environments. - Package-index or mirror compromise when hashes are not verified. Because this dependency handles the Notion bearer token and all outbound synchronization traffic, compromise of the installed package would execute with the user's Python process privileges and gain access to that sensitive data. ### Attack Path 1. An attacker compromises a future `requests` release, an upstream dependency, or the package index/mirror used by the victim. 2. The user runs `pip install -r requirements.txt`. 3. Pip selects the latest available compatible release because no exact version is specified. 4. Malicious package code executes during installation or when imported by the synchronization script. 5. The code can access the process environment, including `NOTION_API_KEY`, inspect synchronized repository metadata, and act with the operating-system privileges of the user. This is a supply-chain hardening weakness rather than evidence that the current `requests` package is malicious. ### Impact Assessment A compromised dependency would execute as the user running the Skill. It could read accessible files and environment variables, steal the Notion token, alter synchronization data, or make ...[truncated 145 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pin `requests` and its transitive dependencies to reviewed versions through a lock file. 2. Generate hashes using a dependency-management tool such as `pip-tools`: ```text requests==<reviewed-version> \ --hash=sha256:<expected-package-hash> ``` 3. Install with hash enforcement: ```bash pip install --require-hashes -r requirements.txt ``` 4. Use automated dependency scanning and update pinned versions through reviewed changes. 5. Install dependencies in an isolated virtual environment rather than into the system Python environment. 6. Use a trusted package index and retain installation provenance for released Skill versions. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (31)

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

Critical
Category
Data Flow
Content
"category": {"multi_select": {}}
        }
    }
    response = requests.post(url, headers=HEADERS, json=payload, verify=False)
    if response.status_code != 200:
        print(f"Error creating database: {response.text}")
        sys.exit(1)
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

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

Critical
Category
Data Flow
Content
"category": {"multi_select": {}}
        }
    }
    response = requests.post(url, headers=HEADERS, json=payload, verify=False)
    if response.status_code != 200:
        print(f"Error creating database: {response.text}")
        sys.exit(1)
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

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

Critical
Category
Data Flow
Content
if next_cursor:
            payload["start_cursor"] = next_cursor
            
        response = requests.post(query_url, headers=HEADERS, json=payload, verify=False)
        if response.status_code != 200:
            print(f"Error querying database for clearing: {response.text}")
            break
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'HEADERS' from os.environ.get (line 20, credential/environment) → requests.patch (network output)

Critical
Category
Data Flow
Content
for page in pages:
            page_id = page["id"]
            patch_url = f"https://api.notion.com/v1/pages/{page_id}"
            requests.patch(patch_url, headers=HEADERS, json={"archived": True}, verify=False)
            count += 1
            
        has_more = data.get("has_more", False)
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
If the associated sync workflow archives all existing Notion pages, uses a hardcoded default parent page ID, and persists mappings locally without clearly disclosing this in the skill, users may unintentionally destroy or alter remote data. In a skill that advertises a simple export-and-sync workflow, undisclosed destructive remote modification materially increases risk.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
If the associated sync workflow archives all existing Notion pages, uses a hardcoded default parent page ID, and persists mappings locally without clearly disclosing this in the skill, users may unintentionally destroy or alter remote data. In a skill that advertises a simple export-and-sync workflow, undisclosed destructive remote modification materially increases risk.

Missing User Warnings

High
Confidence
98% confidence
Finding
The documentation explicitly states that a default fallback Notion API key is hardcoded in the script if the environment variable is unset. Embedded credentials are dangerous because they can be extracted from source, reused by unauthorized parties, and may grant direct access to Notion content or permit unauthorized modifications.

Missing User Warnings

High
Confidence
96% confidence
Finding
When an existing database mapping is found, the script automatically archives all existing pages before repopulating the database, with no confirmation, dry-run mode, or backup step. In an automation skill, this creates a substantial integrity risk because a wrong database ID, stale config entry, or operator mistake can silently destroy user content at scale.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The README instructs users to sync GitHub star data into Notion and notes a local state file, but it does not clearly warn that repository metadata will be transmitted to a third-party service or that persistent local files will be created. This can lead users to disclose potentially sensitive repository interests, names, URLs, and categorization data without informed consent, especially if starred repositories include private, internal, or sensitive topics.

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill declares no explicit tool scope even though it instructs use of environment variables, local file reads/writes, and networked services (GitHub and Notion). Missing scope/permission declarations weakens user awareness and policy enforcement, increasing the chance the skill is run with broader access than intended.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The skill omits a clear warning that syncing sends repository metadata to Notion and modifies third-party remote data. This is dangerous because users may expose private interest patterns, repository metadata, or organizational information to an external service without informed consent.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The manifest explicitly defines a local export operation and a sync operation to an external Notion service, but it provides no user-facing warning, consent gate, or data-handling notice. Even if the data is only GitHub stars metadata, the skill writes locally and transmits externally, which can surprise users, expose private categorization data, or cause unintended disclosure if the source account or destination database is sensitive.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
- If not, run `gh auth login` and follow the prompts.
2. **[jq](https://jqlang.github.io/jq/)**: A lightweight and flexible command-line JSON processor.
   - Mac: `brew install jq`
   - Debian/Ubuntu: `sudo apt-get install jq`
   - Arch Linux: `sudo pacman -S jq`

## Usage
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
- If not, run `gh auth login` and follow the prompts.
2. **[jq](https://jqlang.github.io/jq/)**: A lightweight and flexible command-line JSON processor.
   - Mac: `brew install jq`
   - Debian/Ubuntu: `sudo apt-get install jq`
   - Arch Linux: `sudo pacman -S jq`

## Usage
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The docs mention a local '.notion_sync_config.json' state tracker but do not clearly warn that it stores the mapping between user-supplied database names and Notion database IDs. While not inherently secret, this local state can lead to unintended writes to the wrong database, confusion during reuse on shared systems, or accidental disclosure of workspace structure.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The script reads the Notion API token from the environment and uses it in HTTP Authorization headers for multiple outbound requests, but there is no confirmation prompt or explicit user-facing warning that credentials will be used to create, query, archive, and insert data in Notion. While the script's purpose implies syncing to Notion, it does not clearly disclose credential use or remote data transmission in comments or runtime messaging.

External Transmission

Medium
Category
Data Exfiltration
Content
def create_database(db_name, parent_id):
    """Creates a new Notion database and returns its ID."""
    url = "https://api.notion.com/v1/databases"
    payload = {
        "parent": {"type": "page_id", "page_id": parent_id},
        "title": [
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
def create_database(db_name, parent_id):
    """Creates a new Notion database and returns its ID."""
    url = "https://api.notion.com/v1/databases"
    payload = {
        "parent": {"type": "page_id", "page_id": parent_id},
        "title": [
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
def create_database(db_name, parent_id):
    """Creates a new Notion database and returns its ID."""
    url = "https://api.notion.com/v1/databases"
    payload = {
        "parent": {"type": "page_id", "page_id": parent_id},
        "title": [
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
def create_database(db_name, parent_id):
    """Creates a new Notion database and returns its ID."""
    url = "https://api.notion.com/v1/databases"
    payload = {
        "parent": {"type": "page_id", "page_id": parent_id},
        "title": [
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
"category": {"multi_select": {}}
        }
    }
    response = requests.post(url, headers=HEADERS, json=payload, verify=False)
    if response.status_code != 200:
        print(f"Error creating database: {response.text}")
        sys.exit(1)
Confidence
80% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
"category": {"multi_select": {}}
        }
    }
    response = requests.post(url, headers=HEADERS, json=payload, verify=False)
    if response.status_code != 200:
        print(f"Error creating database: {response.text}")
        sys.exit(1)
Confidence
80% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Unsafe Defaults

Medium
Category
Tool Misuse
Content
"category": {"multi_select": {}}
        }
    }
    response = requests.post(url, headers=HEADERS, json=payload, verify=False)
    if response.status_code != 200:
        print(f"Error creating database: {response.text}")
        sys.exit(1)
Confidence
99% confidence
Finding
The request disables TLS certificate verification with verify=False and globally suppresses related warnings. That allows a man-in-the-middle attacker on the network or a hostile proxy to intercept or modify traffic, including the Notion bearer token and database contents, undermining confidentiality and integrity.

External Transmission

Medium
Category
Data Exfiltration
Content
if next_cursor:
            payload["start_cursor"] = next_cursor
            
        response = requests.post(query_url, headers=HEADERS, json=payload, verify=False)
        if response.status_code != 200:
            print(f"Error querying database for clearing: {response.text}")
            break
Confidence
80% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Unsafe Defaults

Medium
Category
Tool Misuse
Content
if next_cursor:
            payload["start_cursor"] = next_cursor
            
        response = requests.post(query_url, headers=HEADERS, json=payload, verify=False)
        if response.status_code != 200:
            print(f"Error querying database for clearing: {response.text}")
            break
Confidence
99% confidence
Finding
Disabling certificate validation on database query requests permits interception and tampering with responses that drive subsequent archival actions. An attacker could manipulate queried results or steal the bearer token, potentially causing unauthorized data loss or account abuse.

Static analysis

No suspicious patterns detected.