Back to skill

Security audit

X To Notebooklm

Security checks for vulnerabilities and agentic risk

Overview

The skill has a coherent X-to-NotebookLM purpose, but its script can run unintended local shell commands from crafted inputs and leaves uploaded content in temporary files.

Install only if you trust the publisher and are prepared to review or fix the script first. Do not pass private URLs, notebook names, IDs, or environment values to this skill as packaged; a crafted value could run local commands. Treat anything uploaded as stored in NotebookLM and assume fetched content may also remain in local temporary files after a successful run.

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/x-to-notebooklm.mjs:104
Finding

OS Command Injection Through Unvalidated Command-Line Arguments and Environment Configuration

Content
View full analysis

Vulnerability Details

File Location: scripts/x-to-notebooklm.mjs, lines 23-24, 104, 159-162, 182-184, and 272-274
Vulnerability Type: OS command injection through shell command construction
Risk Level: High

Vulnerable Code

js
const NOTEBOOKLM_CLI_PATH = process.env.NOTEBOOKLM_CLI_PATH || 
  `${process.env.HOME}/.openclaw/skills/tiangong-notebooklm-cli/scripts/notebooklm.mjs`;
js
const jinaUrl = `https://r.jina.ai/${url}`;
const result = execCommand(`curl -s "${jinaUrl}"`, timeout);
js
const result = execCommand(
  `node "${NOTEBOOKLM_CLI_PATH}" create "${notebookName}" --json`,
  timeout
);
js
const result = execCommand(
  `node "${NOTEBOOKLM_CLI_PATH}" source add "${tempFile}" --notebook "${notebookId}"`,
  timeout
);
js
const result = execCommand(
  `node "${NOTEBOOKLM_CLI_PATH}" source get "${sourceId}"`,
  timeout
);

The commands are ultimately executed through:

js
function execCommand(command, timeout = 30) {
  try {
    const output = execSync(command, {
      encoding: 'utf-8',
      timeout: timeout * 1000,
      maxBuffer: 10 * 1024 * 1024
    });
    return { success: true, output: output.trim() };
  } catch (error) {
    return {
      success: false,
      error: error.message,
      output: error.stdout?.toString() || '',
      stderr: error.stderr?.toString() || ''
    };
  }
}

Technical Analysis

execSync() executes a command string through a system shell. The script inserts the URL, notebook name, notebook identifier, NotebookLM CLI path, and indirectly parsed source identifier into these shell command strings without shell-safe argument handling.

Placing a value inside double quotes does not make it safe for shell execution. Shell constructs such as command substitution using $() or backticks are still evaluated inside double-quoted strings. Consequently, an att ...[truncated 2576 chars]

Remediation
View remediation

Remediation Suggestions

  1. Replace execSync() command strings with execFileSync() or spawnSync() and pass arguments as an array:

    js
    import { execFileSync } from 'child_process';
    
    function execProgram(program, args, timeout = 30) {
      try {
        const output = execFileSync(program, args, {
          encoding: 'utf-8',
          timeout: timeout * 1000,
          maxBuffer: 10 * 1024 * 1024,
          shell: false
        });
    
        return { success: true, output: output.trim() };
      } catch (error) {
        return {
          success: false,
          error: error.message,
          output: error.stdout?.toString() || '',
          stderr: error.stderr?.toString() || ''
        };
      }
    }
    
  2. Invoke curl without a shell:

    js
    execProgram('curl', ['-s', jinaUrl], timeout);
    
  3. Invoke the NotebookLM CLI with discrete arguments:

    js
    execProgram(
      process.execPath,
      [NOTEBOOKLM_CLI_PATH, 'create', notebookName, '--json'],
      timeout
    );
    
  4. Parse the supplied URL with the standard URL class. Require HTTPS and restrict hosts to the Skill's declared scope:

    js
    const parsed = new URL(url);
    if (parsed.protocol !== 'https:' ||
        !['x.com', 'www.x.com', 'twitter.com', 'www.twitter.com'].includes(parsed.hostname)) {
      throw new Error('Only HTTPS X/Twitter URLs are supported');
    }
    
  5. Validate notebook identifiers against the precise format expected by the NotebookLM CLI. Apply reasonable length limits to notebook names.

  6. Validate NOTEBOOKLM_CLI_PATH as an absolute path to a regular file. Do not pass it through a shell, even after validation.

  7. Constrain --timeout to a finite positive range to prevent unexpected process behavior or denial of service.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/x-to-notebooklm.mjs:126
Finding

Predictable Temporary File and Missing Cleanup After Successful Upload

Content
View full analysis

Vulnerability Details

File Location: scripts/x-to-notebooklm.mjs, lines 126-132 and 176-214
Vulnerability Type: Insecure temporary-file creation and sensitive-data residue
Risk Level: Medium

Vulnerable Code

js
function createTempFile(content, prefix = 'x-article') {
  const filename = `${prefix}-${Date.now()}.md`;
  const filepath = join(tmpdir(), filename);
  
  writeFileSync(filepath, content, 'utf-8');
  
  return filepath;
}
js
function uploadToNotebook(notebookId, content, title, timeout = 30) {
  console.log('📤 正在上传文章到 NotebookLM...');
  
  // 创建临时文件
  const tempFile = createTempFile(content);
  
  try {
    // 使用 source add 命令上传文件(不使用 --json,因为某些版本不支持)
    const result = execCommand(
      `node "${NOTEBOOKLM_CLI_PATH}" source add "${tempFile}" --notebook "${notebookId}"`,
      timeout
    );
    
    if (!result.success) {
      throw new Error(`上传失败:${result.error}`);
    }
    
    // 解析 Source ID - 尝试多种方式
    let sourceId = 'pending';
    
    // 方式 1: 尝试 JSON 解析
    try {
      const source = JSON.parse(result.output);
      sourceId = source.id || source.sourceId || sourceId;
    } catch (e) {
      // 方式 2: 尝试从输出中提取 UUID 格式的 ID
      const uuidMatch = result.output.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i);
      if (uuidMatch) {
        sourceId = uuidMatch[0];
      } else {
        // 方式 3: 尝试 source_xxx 格式
        const match = result.output.match(/source[_-]?[a-zA-Z0-9_-]+/i);
        if (match) {
          sourceId = match[0];
        }
      }
    }
    
    console.log(`✅ 上传成功,Source ID: ${sourceId}`);
    console.log(`📄 临时文件:${tempFile}`);
    
    return { sourceId, tempFile };
  } catch (error) {
    // 清理临时文件
    try {
      unlinkSync(tempFile);
    } catch (e) {}
    throw error;
  }
}

Technical Analysis

The script writes remo ...[truncated 2572 chars]

Remediation
View remediation

Remediation Suggestions

  1. Create a private temporary directory with mkdtempSync() rather than placing predictable files directly in the shared temporary directory.

  2. Create the content file with restrictive permissions and exclusive creation:

    js
    import {
      mkdtempSync,
      writeFileSync,
      rmSync
    } from 'fs';
    import { tmpdir } from 'os';
    import { join } from 'path';
    
    const tempDirectory = mkdtempSync(join(tmpdir(), 'x-to-notebooklm-'));
    const tempFile = join(tempDirectory, 'article.md');
    
    writeFileSync(tempFile, content, {
      encoding: 'utf-8',
      mode: 0o600,
      flag: 'wx'
    });
    
  3. Put cleanup in a finally block so it runs after both successful and failed uploads:

    js
    try {
      // Upload and parse the source identifier.
    } finally {
      rmSync(tempDirectory, {
        recursive: true,
        force: true
      });
    }
    
  4. Do not return or log the temporary path unless it is needed for explicit debugging. If debug logging is necessary, avoid retaining the associated file.

  5. Update REPORT.md so its cleanup claims accurately match the implementation.

  6. Consider avoiding temporary files entirely if the NotebookLM CLI supports standard input or direct text-source submission.

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
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (14)

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
- Main script: `scripts/x-to-notebooklm.mjs`

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
- Main script: `scripts/x-to-notebooklm.mjs`

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

This markdown file presents all usage instructions, warnings, and troubleshooting in Chinese only. That imposes a specific language on users without any opt-in or documented locale justification, which matches the language/locale policy violation category.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The manifest scope is specifically limited to parsing X (Twitter) articles and uploading them to NotebookLM. However, the report's own output example uses a GitHub URL, indicating the implementation is actually a more general webpage-to-NotebookLM uploader rather than an X-specific tool.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The stated purpose is to handle X/Twitter articles, but the documented test case uploads content from https://github.com/openclaw/openclaw instead. This indicates the implemented behavior accepts broader web content than the manifest claims.

Content

No source excerpt is available for this finding.

Skill Enumeration

Medium
Category
Agent Snooping
Confidence
80% confidence
Finding

Skill enumerates or reads other installed skills. Access to other skills' SKILL.md files or the skills directory reveals prompt instructions, capabilities, and secrets that should be invisible to peer skills.

Content

Scanner excerpt · REPORT.md (reported line 225)May include surrounding context.

md
## 📚 相关文件

- **Skill 文档**: `~/.openclaw/workspace/skills/x-to-notebooklm/SKILL.md`
- **主脚本**: `~/.openclaw/workspace/skills/x-to-notebooklm/scripts/x-to-notebooklm.mjs`
- **元数据**: `~/.openclaw/workspace/skills/x-to-notebooklm/_meta.json`
- **NotebookLM CLI 文档**: `~/.openclaw/skills/tiangong-notebooklm-cli/references/cli-commands.md`

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding

The skill documents shell execution and environment-variable use but does not declare any explicit tool scope or permissions boundary. This increases the chance that an agent may invoke broader shell capabilities than users expect, enabling unintended command execution or access to local environment data during skill use.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The skill description does not clearly warn that submitted URLs and fetched content are sent to external services, specifically r.jina.ai and Google NotebookLM. Users may unknowingly submit sensitive or private material, resulting in third-party disclosure, retention, or processing outside their expected trust boundary.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
78% confidence
Finding

The workflow explicitly creates or reuses persistent NotebookLM notebooks and uploads article content as sources, meaning imported data may remain stored across sessions. Persistent storage is risky when users assume a transient fetch-and-analyze flow, especially if the content is sensitive or if notebooks are shared or later reused unintentionally.

Content

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

md
**English:**
1. **Fetch Content** - Use `curl + r.jina.ai` to extract plain text from X articles
2. **Create Notebook** - Automatically create a new NotebookLM Notebook (or reuse existing)
3. **Upload Article** - Upload parsed content as Source to Notebook
4. **Verify** - Check upload status and return Notebook ID and Source ID

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The documentation presents the skill as limited to X/Twitter articles, but the test case shows successful ingestion of a GitHub URL, indicating likely arbitrary-URL handling. This mismatch is dangerous because operators may trust the narrower description while the skill can transmit other web content to third-party services and NotebookLM, expanding data exposure and misuse potential.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The natural-language instructions, option descriptions, help text, and runtime messages are presented exclusively in Chinese. There is no opt-in, language selection mechanism, or documented reason that this skill must be Chinese-only, which can violate organizational language/locale policy.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The manifest describes a focused workflow of fetching X content and uploading it to NotebookLM, but the implementation uses child_process execution for curl and for invoking an external NotebookLM CLI script. Spawning shell commands and relying on another locally configured script is a more powerful capability than the stated document-processing purpose and is not disclosed in the manifest description.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The script forwards a user-supplied X URL to r.jina.ai and then uploads the fetched content to NotebookLM, causing third-party data transmission without explicit consent or clear disclosure at runtime. This can expose sensitive URLs, private query tokens, or article contents to external services and creates a meaningful privacy and data-handling risk.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
83% confidence
Finding

This code creates a markdown file in the system temp directory containing the fetched X article content. While the script logs the temp file path after upload, the user-facing usage/help text does not warn in advance that content will be written to local storage as part of the process.

Content

No source excerpt is available for this finding.

Static analysis

Detected: suspicious.dangerous_exec

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/x-to-notebooklm.mjs:85