Back to skill

Security audit

Bilibili Notion Pipeline Skill

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its stated Bilibili-to-Notion purpose, but it has broad upload and deletion capabilities that need user review before installation.

Review the upload destination, require HTTPS, and use a narrowly scoped upload token before running this skill. Avoid --replace-children unless you are certain the Notion page can have all existing top-level blocks archived. Only run cleanup against metadata produced by this pipeline and consider using cleanup-mode none until paths have been inspected.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/pipeline.py:443
Finding
Arbitrary File Deletion Through Untrusted Metadata Paths<![CDATA[ ## Vulnerability Details **File Location**: `scripts/pipeline.py:443-461`, with attacker-controlled metadata loaded at `scripts/pipeline.py:631-637` **Vulnerability Type**: Improper validation of file paths used for deletion **Risk Level**: High ### Vulnerable Code ```python def cleanup_from_meta(meta: Dict[str, Any], mode: str = "temp") -> Dict[str, Any]: deleted: List[str] = [] kept: List[str] = [] for key in ("wav_path", "transcript_path"): path_value = meta.get(key) if not path_value: continue path = Path(path_value) if mode in {"temp", "all"} and path.exists(): path.unlink() deleted.append(str(path)) elif path.exists(): kept.append(str(path)) video_value = meta.get("local_file") if video_value: video_path = Path(video_value) if mode == "all" and video_path.exists(): video_path.unlink() deleted.append(str(video_path)) elif video_path.exists(): kept.append(str(video_path)) ``` The affected cleanup command loads the paths directly from a user-selected metadata file: ```python def cmd_cleanup(args: argparse.Namespace) -> None: meta = load_metadata(Path(args.metadata)) mode = "all" if args.delete_video else args.mode result = cleanup_from_meta(meta, mode) meta["cleanup"] = result meta["state"] = "cleaned" if mode != "none" else meta.get("state", "prepared") save_metadata(meta, meta.get("bvid", "metadata"), Path(meta["metadata_path"])) print(json.dumps(result, ensure_ascii=False, indent=2)) ``` ### Technical Analysis The cleanup implementation treats the `wav_path`, `transcript_path`, and `local_file` properties in the metadata JSON as trusted filesystem paths. It does not canonicalize these paths or verify that they are located beneath the intended `TEMP_DIR` and `DOWNLOAD_DIR` directories. Because the `--metadata` option accepts an arbitrary JSON fil ...[truncated 1831 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Canonicalize every candidate with `Path.resolve(strict=True)` before deletion. 2. Permit deletion only when the resolved target is beneath an explicitly approved root: - `TEMP_DIR` for WAV, transcript, and segment artifacts - `DOWNLOAD_DIR` for downloaded video files 3. Reject absolute or traversal-based metadata paths that resolve outside those roots. 4. Validate that the target has the expected file type and naming pattern, such as a recognized BVID and the expected extension. 5. Do not trust `metadata_path` stored inside the metadata document. Continue writing to the validated path supplied through `--metadata`, or require metadata files to reside in a dedicated state directory. 6. Use a strict metadata schema and reject unknown, missing, or malformed fields. 7. Consider recording artifact identifiers or paths relative to trusted roots rather than storing unrestricted absolute paths. 8. Before destructive cleanup, display the resolved deletion targets and require explicit confirmation when invoked interactively. 9. Add security tests covering absolute paths, `..` traversal, paths outside approved roots, and maliciously modified metadata. A suitable containment check should follow this model: ```python def require_path_beneath(candidate: Path, root: Path) -> Path: resolved = candidate.resolve(strict=True) trusted_root = root.resolve(strict=True) if not resolved.is_relative_to(trusted_root): raise RuntimeError(f"Refusing to delete out-of-scope path: {resolved}") if not resolved.is_file(): raise RuntimeError(f"Refusing to delete non-file target: {resolved}") return resolved ``` ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/pipeline.py:225
Finding
Upload Token and Video Can Be Transmitted Over Plaintext HTTP<![CDATA[ ## Vulnerability Details **File Location**: `scripts/pipeline.py:225-235` **Vulnerability Type**: Sensitive data transmitted without enforcing transport encryption **Risk Level**: Medium ### Vulnerable Code ```python def upload_video(video_path: Path) -> Optional[str]: if not UPLOAD_URL or not UPLOAD_TOKEN: return None with video_path.open("rb") as fh: resp = requests.post( UPLOAD_URL, headers={"Authorization": f"Bearer {UPLOAD_TOKEN}"}, files={"file": (video_path.name, fh, "video/mp4")}, timeout=120, ) resp.raise_for_status() payload = resp.json() if isinstance(payload, list) and payload: item = payload[0] src = item.get("src") or item.get("url") if src: if src.startswith("http"): return src base = re.match(r"^(https?://[^/]+)", UPLOAD_URL) if base: return base.group(1) + src if isinstance(payload, dict): for key in ("url", "src", "download_url"): if payload.get(key): return payload[key] raise RuntimeError(f"Upload succeeded but no public URL found: {payload}") ``` ### Technical Analysis The upload operation is part of the Skill's declared functionality, and no covert or unrelated exfiltration destination was identified. However, `UPLOAD_URL` is taken from the environment and used without scheme validation. The implementation therefore permits an `http://` endpoint. When plaintext HTTP is used, the request exposes both: - The bearer credential in the `Authorization` header - The complete MP4 file in the multipart request body The `requests` library verifies TLS certificates for HTTPS destinations by default, but that protection is irrelevant when the configured URL uses HTTP. The code also accepts returned URLs beginning with either HTTP or HTTPS, which can propagate an insecure public download URL into Notion. ### Atta ...[truncated 1501 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse `UPLOAD_URL` before opening the video and reject every scheme except `https`. 2. Require a valid hostname and reject URLs containing embedded credentials. 3. Consider an explicit allowlist of trusted upload hosts, particularly for automated agent execution. 4. Continue using normal TLS certificate validation and do not add `verify=False`. 5. Reject or warn about upload responses that provide an `http://` download URL. 6. Document clearly that the MP4 is disclosed to the configured third-party upload provider and may become publicly accessible. 7. Use narrowly scoped, revocable, short-lived upload tokens where supported. 8. Avoid exposing tokens in logs, exception output, metadata, or command-line arguments. 9. Add tests proving that HTTP, malformed URLs, and unexpected hosts are rejected before any file content or authorization header is transmitted. 10. Where feasible, require explicit user confirmation before uploading potentially private media to a newly configured host. Example validation: ```python from urllib.parse import urlparse def validate_upload_url(value: str) -> str: parsed = urlparse(value) if parsed.scheme.lower() != "https": raise RuntimeError("UPLOAD_URL must use HTTPS") if not parsed.hostname: raise RuntimeError("UPLOAD_URL must contain a valid hostname") if parsed.username or parsed.password: raise RuntimeError("Embedded URL credentials are not permitted") return value ``` ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (21)

Tainted flow: 'UPLOAD_URL' from os.getenv (line 30, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Content
if not UPLOAD_URL or not UPLOAD_TOKEN:
        return None
    with video_path.open("rb") as fh:
        resp = requests.post(
            UPLOAD_URL,
            headers={"Authorization": f"Bearer {UPLOAD_TOKEN}"},
            files={"file": (video_path.name, fh, "video/mp4")},
Confidence
94% confidence
Finding
The skill uploads a local video file and bearer token to a fully environment-configurable UPLOAD_URL with no allowlist, origin validation, or user confirmation. If that endpoint is misconfigured or malicious, the pipeline can exfiltrate downloaded media and credentials to an attacker-controlled service, which is especially risky because upload is automatic during normal execution.

Tainted flow: 'NOTION_DATABASE_ID' from os.getenv (line 29, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Content
if not NOTION_DATABASE_ID:
        raise RuntimeError("NOTION_DATABASE_ID is required when creating a new page")

    resp = requests.post(
        "https://api.notion.com/v1/pages",
        headers=headers,
        json={"parent": {"database_id": NOTION_DATABASE_ID}, "properties": props},
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
99% confidence
Finding
The declared description presents an end-to-end ingestion pipeline for Bilibili content into Notion, involving external resource access and multiple workflow steps. The supplied code does not perform any of those actions. It is a local text-processing utility that parses Markdown lines and emits Notion-compatible block dictionaries. While this could be a supporting helper for the described system, the code chunk itself does not implement the declared primary purpose and instead has a much narrower, materially different function.

Missing User Warnings

High
Confidence
97% confidence
Finding
Archiving existing Notion page children is a destructive remote action performed without an interactive confirmation or a strong safety check. In practice, a mistaken page_id or accidental use of --replace-children can wipe visible page content and create significant integrity and availability issues for user data.

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill describes operations that inherently require shell, filesystem, environment access, and network connectivity, but it does not declare any explicit tool scope or permission boundaries. This creates an over-privilege and reviewability problem: a host agent may grant broader capabilities than necessary, making misuse, secret exposure, or unintended destructive actions harder to constrain.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
The description text specifies the skill's use cases entirely in Chinese phrasing, which indicates a language-specific interaction expectation. There is no accompanying note that the skill supports multiple languages or that Chinese is optional, so this creates a natural-language locale constraint without user opt-in.

Natural-Language Policy Violations

Medium
Confidence
88% confidence
Finding
Most operational instructions, triggers, and workflow guidance are presented only in Chinese, which effectively imposes a language requirement on users and operators. The file does not explain that this locale restriction is intentional for a region-specific compliance or product need, nor does it offer an alternative language path.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The workflow explicitly includes uploading an mp4 and obtaining a public URL, but the notes provide no warning, consent gate, or privacy controls around making user media publicly accessible. In this skill context, videos may contain copyrighted, private, or sensitive content, so automatically exposing them via a public link creates a real confidentiality and compliance risk.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def run(cmd: List[str]) -> None:
    subprocess.run(cmd, check=True)


def extract_bvid(value: str) -> str:
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def audio_duration_seconds(path: Path) -> float:
    result = subprocess.run(
        [
            "ffprobe", "-v", "error",
            "-show_entries", "format=duration",
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The code transmits a local MP4 to a remote endpoint without an explicit warning or consent step at the point of upload. In this skill context, that is dangerous because users may expect local processing plus Notion sync, but not secondary transfer of the full video to an arbitrary configured service.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The pipeline sends transcript content, video metadata, and page updates to Notion, an external service, without an explicit warning at the transmission point. This aligns with the advertised purpose, so it is not covert exfiltration, but it is still a real privacy and data-handling concern because potentially sensitive transcript text is uploaded automatically.

External Transmission

Medium
Category
Data Exfiltration
Content
if page_id:
        resp = requests.patch(
            f"https://api.notion.com/v1/pages/{page_id}",
            headers=headers,
            json={"properties": props},
            timeout=60,
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
if page_id:
        resp = requests.patch(
            f"https://api.notion.com/v1/pages/{page_id}",
            headers=headers,
            json={"properties": props},
            timeout=60,
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
if page_id:
        resp = requests.patch(
            f"https://api.notion.com/v1/pages/{page_id}",
            headers=headers,
            json={"properties": props},
            timeout=60,
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
if page_id:
        resp = requests.patch(
            f"https://api.notion.com/v1/pages/{page_id}",
            headers=headers,
            json={"properties": props},
            timeout=60,
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
if page_id:
        resp = requests.patch(
            f"https://api.notion.com/v1/pages/{page_id}",
            headers=headers,
            json={"properties": props},
            timeout=60,
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
if not NOTION_DATABASE_ID:
        raise RuntimeError("NOTION_DATABASE_ID is required when creating a new page")

    resp = requests.post(
        "https://api.notion.com/v1/pages",
        headers=headers,
        json={"parent": {"database_id": NOTION_DATABASE_ID}, "properties": props},
Confidence
70% 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
if not NOTION_DATABASE_ID:
        raise RuntimeError("NOTION_DATABASE_ID is required when creating a new page")

    resp = requests.post(
        "https://api.notion.com/v1/pages",
        headers=headers,
        json={"parent": {"database_id": NOTION_DATABASE_ID}, "properties": props},
Confidence
80% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Description-Behavior Mismatch

Medium
Confidence
96% confidence
Finding
The skill supports archiving all existing top-level children on a Notion page when --replace-children is used, which is a destructive content-modification capability beyond simple append behavior. In a pipeline meant to organize content, this can silently remove prior page material and cause data loss if the page ID is wrong or the operator misunderstands the flag.

Context-Inappropriate Capability

Low
Confidence
77% confidence
Finding
The code invokes external binaries such as ffmpeg, ffprobe, and potentially the whisper CLI via subprocess to process media and transcription. While media processing is related to the pipeline's purpose, spawning host binaries is a stronger capability than the manifest text communicates and may be contextually surprising for a skill framed mainly as a Bilibili-to-Notion content pipeline.

Static analysis

No suspicious patterns detected.