T09 · Insecure Skill Coding Practices
- Location
scripts/sync_stars_to_notion_db.py:91- Finding
TLS Certificate Verification Is Disabled for Authenticated Notion API Requests
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
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.
scripts/sync_stars_to_notion_db.py:91TLS Certificate Verification Is Disabled for Authenticated Notion API Requests
scripts/sync_stars_to_notion_db.py:97Existing Notion Records Are Archived Before Input Is Validated
scripts/sync_stars_to_notion_db.py:18Opaque Hardcoded Notion Parent Page Is Used as the Default Data Destination
requirements.txt:1Python Dependency Is Not Version-Pinned
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
"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)
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
"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)
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
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
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
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)
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.
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.
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.
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.
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.
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.
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.
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.
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.
- 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
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.
- 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
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.
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.
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
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": [
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
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": [
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
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": [
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
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": [
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
"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)
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
"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)
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.
"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)
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
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
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.
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
No suspicious patterns detected.