Back to skill

Security audit

OpenClaw Skill: Obsidian Markdown to Cloudflare Pages

Security checks for vulnerabilities and agentic risk

Overview

This skill has a coherent publishing purpose, but its implementation can run unpinned third-party code, mishandle credentials, and delete or mutate local files more broadly than users may expect.

Install only if you are comfortable auditing and hardening the script first. Use a dedicated empty workspace, scoped Cloudflare token, uncommitted secret files, test project/subdomain, and avoid Basic Auth passwords in config until credential handling and generated middleware are fixed.

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

T09 · Insecure Skill Coding Practices

Error
Location
bin/publishmd-cf.js:41
Finding
Shell Command Injection Through Untrusted Configuration Values<![CDATA[ ## Vulnerability Details **File Location**: `bin/publishmd-cf.js:41-55, 301-307, 511-523` **Vulnerability Type**: OS command injection **Risk Level**: High ### Complete Code Snippet ```javascript function sh(command, cwd, quiet = false) { if (DRY_RUN && !quiet) { const redacted = command .replace(/CLOUDFLARE_API_TOKEN="[^"]*"/g, 'CLOUDFLARE_API_TOKEN="***"') .replace(/BASIC_AUTH_PASSWORD="[^"]*"/g, 'BASIC_AUTH_PASSWORD="***"'); console.log(`[dry-run] ${redacted}`); return ''; } if (quiet) return execSync(command, { stdio: ['ignore', 'pipe', 'pipe'], cwd: cwd || process.cwd() }).toString(); execSync(command, { stdio: 'inherit', cwd: cwd || process.cwd() }); } ``` ```javascript const excludes = (cfg.source.excludeFolders || []) .map((f) => `--exclude '${f}/'`) .join(' '); sh(`rsync -av --exclude '.obsidian/' --exclude '*.canvas' ${excludes} "${src}/" "${dest}/${folder}/"`); ``` ```javascript const envPrefix = [ `CLOUDFLARE_API_TOKEN="${token.replace(/"/g, '\\"')}"`, accountId ? `CLOUDFLARE_ACCOUNT_ID="${accountId.replace(/"/g, '\\"')}"` : '' ].filter(Boolean).join(' '); sh(`${envPrefix} npx wrangler pages deploy public --project-name "${project}" --branch "${branch}"`, workspaceDir); ``` ### Technical Analysis The `sh()` helper passes dynamically constructed strings to `execSync()`, which invokes a shell. Multiple values originating from `config.json`, `.env`, or wizard input are interpolated into these strings. Quoting does not provide adequate protection. Values placed inside double quotes can still contain shell substitutions such as `$(command)` or backticks. The Cloudflare token handling escapes only double-quote characters and does not neutralize command substitutions, backslashes, newlines, or other shell syntax. The single-quoted `rsync` exclusion values can escape their quoting context by containing a single quote. Affected values include source and exclusion folder names, workspace paths, Cloudfl ...[truncated 1175 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Replace shell-string execution with `execFileSync()` or `spawnSync()` and explicit argument arrays. 2. Invoke `rsync`, `git`, `npm`, `npx`, and `wrangler` directly without `shell: true`. 3. Supply Cloudflare credentials using the child process `env` option rather than embedding them in a command prefix. 4. Validate folder names, project names, and branch names against strict expected formats. 5. Reject control characters, newlines, traversal segments, and unexpected absolute paths. 6. Do not attempt to implement shell escaping manually; argument-array APIs avoid shell interpretation entirely. 7. Add tests containing quotes, command substitutions, semicolons, newlines, and backticks to verify that values are treated only as literal arguments. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
bin/publishmd-cf.js:288
Finding
Configurable Recursive Deletion Can Escape the Quartz Workspace<![CDATA[ ## Vulnerability Details **File Location**: `bin/publishmd-cf.js:58-76, 288-300` **Vulnerability Type**: Path traversal leading to arbitrary recursive deletion **Risk Level**: High ### Complete Code Snippet ```javascript function assertSafePath(targetPath, label = 'path') { const resolved = path.resolve(targetPath); if (resolved === '/' || resolved === os.homedir() || resolved.length < 8) { throw new Error(`Refusing unsafe ${label}: ${resolved}`); } return resolved; } function clearDirectoryContents(dir) { const resolved = assertSafePath(dir, 'directory clear target'); if (!fs.existsSync(resolved)) return; for (const entry of fs.readdirSync(resolved)) { const p = path.join(resolved, entry); if (DRY_RUN) { console.log(`[dry-run] remove ${p}`); continue; } fs.rmSync(p, { recursive: true, force: true }); } } ``` ```javascript function sync() { const cfg = loadConfig(); const vaultPath = expandHome(cfg.source.vaultPath); const workspaceDir = expandHome(cfg.publish.workspaceDir); const contentDir = cfg.publish.contentDir || 'content'; const dest = path.join(workspaceDir, contentDir); if (!cfg.source.includeFolders?.length) throw new Error('includeFolders is empty'); fs.mkdirSync(dest, { recursive: true }); clearDirectoryContents(dest); ``` ### Technical Analysis The synchronization routine recursively clears the configured destination before copying content. The destination is calculated with `path.join(workspaceDir, contentDir)`, but `contentDir` is not required to be a simple relative child path. It can contain `..` traversal segments or otherwise resolve outside the configured workspace. `assertSafePath()` only rejects the filesystem root, the exact home directory, and paths shorter than eight characters. It does not verify that the resolved deletion target remains under `workspaceDir`. Consequently, an unrelated directory such as `/home/user/Documents` can pass the guard. The norm ...[truncated 1252 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Resolve `workspaceDir` and `dest` to canonical absolute paths before any mutation. 2. Require `contentDir` to be a non-empty relative path and reject absolute paths and all `..` components. 3. Verify containment using a separator-aware check, such as ensuring the destination starts with `resolvedWorkspace + path.sep`. 4. Prefer requiring the destination to equal one explicitly configured content directory rather than accepting arbitrary paths. 5. Reject symlinked workspace or destination paths, or validate their `realpath()` results before deletion. 6. Require explicit confirmation or a dedicated destructive flag before clearing a non-empty directory. 7. Record and display the exact canonical deletion target before proceeding. 8. Add tests for traversal, absolute paths, symlinks, home-directory descendants, and paths sharing only a string prefix with the workspace. ]]>

T08 · Insecure Dependencies

Warning
Location
bin/publishmd-cf.js:342
Finding
Unpinned Third-Party Code Is Downloaded and Executed<![CDATA[ ## Vulnerability Details **File Location**: `bin/publishmd-cf.js:342-365, 437-439, 517-523` **Vulnerability Type**: Insecure software supply-chain execution **Risk Level**: Medium ### Complete Code Snippet ```javascript // Preferred path (older Quartz bootstrap flow). let bootstrapOk = false; try { if (!fs.existsSync(path.join(workspaceDir, 'package.json'))) { sh('npm init -y', workspaceDir); } sh('npx quartz create', workspaceDir); bootstrapOk = fs.existsSync(path.join(workspaceDir, 'quartz.config.ts')); } catch { bootstrapOk = false; } // Fallback path: clone Quartz directly if bootstrap command is unavailable. if (!bootstrapOk) { console.log('Falling back to git clone bootstrap for Quartz...'); if (fs.readdirSync(workspaceDir).length > 0) { if (!ALLOW_DESTRUCTIVE) { throw new Error('Fallback bootstrap requires clearing workspaceDir. Re-run with ALLOW_DESTRUCTIVE=1 or use an empty workspace directory.'); } clearDirectoryContents(workspaceDir); } sh(`git clone https://github.com/jackyzha0/quartz.git "${workspaceDir}"`); sh('npm i', workspaceDir); bootstrapOk = fs.existsSync(path.join(workspaceDir, 'quartz.config.ts')); } ``` ```javascript function build() { const cfg = loadConfig(); const workspaceDir = expandHome(cfg.publish.workspaceDir); sh('npx quartz build', workspaceDir); ``` ```javascript sh(`${envPrefix} npx wrangler pages deploy public --project-name "${project}" --branch "${branch}"`, workspaceDir); ``` ### Technical Analysis The workflow uses `npx` without explicit package versions and can clone the mutable default branch of the Quartz repository. It subsequently runs `npm i`, which can execute package lifecycle scripts. No reviewed lockfile, commit hash, release tag, package integrity value, or signature is enforced by this project. As a result, the effective code executed during setup, build, or deployment can differ from the code available when this Skill was audited. The referenc ...[truncated 1299 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pin Quartz and Wrangler to reviewed, explicit versions. 2. Pin the Quartz Git source to an immutable commit hash and verify that commit before execution. 3. Include a reviewed lockfile and use `npm ci` rather than an unconstrained `npm i`. 4. Avoid implicit package downloads through bare `npx` commands; install audited dependencies ahead of time and invoke their local binaries. 5. Consider initially installing with lifecycle scripts disabled, then explicitly permit only required scripts after review. 6. Enable automated dependency vulnerability and integrity monitoring. 7. Document the exact trusted versions and establish a controlled process for reviewing upgrades. 8. Where supported, verify package provenance, signatures, checksums, or repository commit signatures. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
bin/publishmd-cf.js:483
Finding
Basic Authentication Credentials Are Persisted and Embedded in Plaintext<![CDATA[ ## Vulnerability Details **File Location**: `bin/publishmd-cf.js:183-191, 218-228, 483-501` **Vulnerability Type**: Plaintext credential storage and exposure **Risk Level**: Medium ### Complete Code Snippet ```javascript const authEnabled = await ask( 'Enable basic auth protection? (y/n)', defaults?.cloudflare?.basicAuth?.enabled === false ? 'n' : 'y' ); let authUsername = defaults?.cloudflare?.basicAuth?.username || ''; let authPassword = defaults?.cloudflare?.basicAuth?.password || ''; if (/^y(es)?$/i.test(authEnabled)) { authUsername = await ask('Basic auth username', authUsername || '1851'); authPassword = await ask('Basic auth password', authPassword || ''); } ``` ```javascript basicAuth: { enabled: /^y(es)?$/i.test(authEnabled), username: authUsername, password: authPassword } ``` ```javascript const usernameEnv = auth?.usernameEnv || 'BASIC_AUTH_USERNAME'; const passwordEnv = auth?.passwordEnv || 'BASIC_AUTH_PASSWORD'; const username = process.env[usernameEnv] || auth.username; const password = process.env[passwordEnv] || auth.password; if (!username || !password) { throw new Error(`basicAuth is enabled but credentials are missing. Set config values or env vars ${usernameEnv}/${passwordEnv}.`); } fs.mkdirSync(fnDir, { recursive: true }); const middleware = `const USER = ${JSON.stringify(username)};\nconst PASS = ${JSON.stringify(password)};\n\nfunction unauthorized() {\n return new Response("Authentication required", {\n status: 401,\n headers: {\n "WWW-Authenticate": 'Basic realm="Private Vault", charset="UTF-8"',\n "Cache-Control": "no-store",\n },\n });\n}\n\nexport async function onRequest(context) {\n const auth = context.request.headers.get("Authorization") || "";\n if (!auth.startsWith("Basic ")) return unauthorized();\n\n try {\n const decoded = atob(auth.slice(6));\n const [user, ...rest] = decoded.split(":");\n const pass = rest.join(":");\n\n if (user !== USER || pass !== PASS) ...[truncated 1916 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove password fields from persistent configuration and never serialize credentials into generated source code. 2. Store credentials using Cloudflare encrypted secrets or runtime secret bindings. 3. Make middleware read credentials from the runtime environment instead of embedding literal values. 4. Use a hidden-input library for interactive password entry. 5. Set restrictive permissions on secret files and generated artifacts where temporary local material is unavoidable. 6. Ensure `config/config.json`, `.env`, generated middleware containing secrets, and relevant build artifacts are excluded from source control. 7. Add automated secret scanning before commits and deployments. 8. Rotate any credentials that may already have been committed, archived, or shared. 9. Consider a stronger identity-aware access mechanism for sensitive content instead of static shared Basic Auth credentials. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
bin/publishmd-cf.js:437
Finding
Dry-Run Mode Performs Filesystem Mutations Despite Non-Mutating Documentation<![CDATA[ ## Vulnerability Details **File Location**: `bin/publishmd-cf.js:41-55, 288-300, 330-334, 437-468` **Vulnerability Type**: Incomplete safety-control implementation **Risk Level**: Low ### Complete Code Snippet ```javascript function sh(command, cwd, quiet = false) { if (DRY_RUN && !quiet) { const redacted = command .replace(/CLOUDFLARE_API_TOKEN="[^"]*"/g, 'CLOUDFLARE_API_TOKEN="***"') .replace(/BASIC_AUTH_PASSWORD="[^"]*"/g, 'BASIC_AUTH_PASSWORD="***"'); console.log(`[dry-run] ${redacted}`); return ''; } if (quiet) return execSync(command, { stdio: ['ignore', 'pipe', 'pipe'], cwd: cwd || process.cwd() }).toString(); execSync(command, { stdio: 'inherit', cwd: cwd || process.cwd() }); } ``` ```javascript const dest = path.join(workspaceDir, contentDir); if (!cfg.source.includeFolders?.length) throw new Error('includeFolders is empty'); fs.mkdirSync(dest, { recursive: true }); clearDirectoryContents(dest); ``` ```javascript function setupProject() { const cfg = loadConfig(); const workspaceDir = expandHome(cfg.publish.workspaceDir); fs.mkdirSync(workspaceDir, { recursive: true }); ``` ```javascript function build() { const cfg = loadConfig(); const workspaceDir = expandHome(cfg.publish.workspaceDir); sh('npx quartz build', workspaceDir); const publicDir = path.join(workspaceDir, 'public'); const rootSourceFolder = cfg?.site?.branding?.rootSourceFolder || 'Clippings'; const sourceIndex = path.join(publicDir, rootSourceFolder, 'index.html'); const rootIndex = path.join(publicDir, 'index.html'); if (fs.existsSync(sourceIndex)) { fs.copyFileSync(sourceIndex, rootIndex); console.log(`Promoted /${rootSourceFolder}/index.html -> /index.html ✅`); } // Config-driven branding. const domain = inferDomain(cfg); const clippingsLabel = cfg?.site?.branding?.clippingsIndexLabel || `${rootSourceFolder} | ${domain}`; const rootLabel = cfg?.site?.branding?.rootIndexLabel || `Vault | ${domain} ...[truncated 1965 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Route every mutating filesystem operation through centralized dry-run-aware wrappers. 2. Return from mutation-oriented workflows before any `mkdirSync`, `copyFileSync`, `writeFileSync`, `unlinkSync`, or `rmSync` call when `DRY_RUN` is active. 3. Separate planning from execution: first calculate and display an action plan, then execute it only when dry-run is disabled. 4. Ensure post-build HTML transformations do not run when the build command was skipped. 5. Add integration tests that snapshot the filesystem before and after every command in dry-run mode and require byte-for-byte equality. 6. Clearly report actions that would occur, including canonical paths, without touching those paths. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
Findings (21)

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The declared description understates the skill's real capabilities, which include reading local Obsidian configuration, modifying generated site content, handling credentials, and performing destructive cleanup. This mismatch is dangerous because users or orchestrators may grant trust based on the benign description while the skill performs broader filesystem, network, and secret-handling actions.

Credential Access

High
Category
Privilege Escalation
Content
### Option B: skill-local env file (recommended for this skill)

```bash
cp skills/obsidian-cloudflare-pages/.env.example skills/obsidian-cloudflare-pages/.env
# then edit .env
# optional auth envs: BASIC_AUTH_USERNAME / BASIC_AUTH_PASSWORD
```
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
### Option B: skill-local env file (recommended for this skill)

```bash
cp skills/obsidian-cloudflare-pages/.env.example skills/obsidian-cloudflare-pages/.env
# then edit .env
# optional auth envs: BASIC_AUTH_USERNAME / BASIC_AUTH_PASSWORD
```
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
### Option B: skill-local env file (recommended for this skill)

```bash
cp skills/obsidian-cloudflare-pages/.env.example skills/obsidian-cloudflare-pages/.env
# then edit .env
# optional auth envs: BASIC_AUTH_USERNAME / BASIC_AUTH_PASSWORD
```
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
### Option B: skill-local env file (recommended for this skill)

```bash
cp skills/obsidian-cloudflare-pages/.env.example skills/obsidian-cloudflare-pages/.env
# then edit .env
# optional auth envs: BASIC_AUTH_USERNAME / BASIC_AUTH_PASSWORD
```
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
### Option B: skill-local env file (recommended for this skill)

```bash
cp skills/obsidian-cloudflare-pages/.env.example skills/obsidian-cloudflare-pages/.env
# then edit .env
# optional auth envs: BASIC_AUTH_USERNAME / BASIC_AUTH_PASSWORD
```
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
### Option B: skill-local env file (recommended for this skill)

```bash
cp skills/obsidian-cloudflare-pages/.env.example skills/obsidian-cloudflare-pages/.env
# then edit .env
# optional auth envs: BASIC_AUTH_USERNAME / BASIC_AUTH_PASSWORD
```
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Ae1

High
Category
analysis-evasion
Content
node bin/publishmd-cf.js init
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node bin/publishmd-cf.js init
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node bin/publishmd-cf.js init
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Credential Access

High
Category
Privilege Escalation
Content
}

// Load skill-local env vars if present (without overriding shell env)
loadDotEnv(path.join(root, '.env'));

function expandHome(p) {
  return p?.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : p;
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Session Persistence

Medium
Category
Rogue Agent
Content
Commands:

- `init` — create config from example
- `wizard` — interactive setup
- `setup-project` — initialize Quartz workspace if needed
- `doctor` — dependency/env/path checks
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Lp3

Medium
Category
MCP Least Privilege
Confidence
86% confidence
Finding
The skill advertises commands that use environment variables for Cloudflare tokens and local .env loading, but it does not declare any explicit tool scope or permissions boundaries. In an agent setting, missing scope metadata can cause overbroad execution or secret access assumptions, making it harder to constrain what the skill may read from the environment.

Rp1

Medium
Category
MCP Rug Pull
Confidence
88% confidence
Finding
Referencing `npx quartz` without pinning a version introduces supply-chain risk because the executed package can change over time or resolve unexpectedly from the registry. That can lead to unreviewed code execution during build/setup, especially dangerous in a skill that also handles local files and deployment credentials.

Session Persistence

Medium
Category
Rogue Agent
Content
## Cloudflare API token setup (recommended)

Create a Cloudflare API token with at least:
- **Account → Cloudflare Pages:Edit**
- (Optional) **Zone → DNS:Edit** if you want DNS automation elsewhere
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Rp1

Medium
Category
MCP Rug Pull
Confidence
92% confidence
Finding
The script executes `npx quartz create` without pinning a specific package version. This allows the latest package (or a compromised package release) to be fetched and executed at runtime, creating a supply-chain execution risk on the operator's machine.

Context-Inappropriate Capability

Medium
Confidence
94% confidence
Finding
The fallback bootstrap clones a remote GitHub repository and runs `npm i`, which executes untrusted third-party code and install scripts on the local machine. This is a significant supply-chain risk, especially because it occurs automatically during setup and is not pinned to a specific commit or release.

Rp1

Medium
Category
MCP Rug Pull
Confidence
90% confidence
Finding
The build step runs `npx quartz build` without pinning the package version used for execution. If Quartz is not already locally installed as a locked dependency, `npx` may resolve and execute an unexpected or malicious version, which is particularly risky because it processes local vault content and runs on the user's workstation.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The middleware generator embeds the basic-auth username and password directly into `_middleware.js`, writing secrets into source files in the workspace. This increases the chance of accidental disclosure through source control, backups, logs, or later publication of the workspace contents.

Rp1

Medium
Category
MCP Rug Pull
Confidence
94% confidence
Finding
The deployment step invokes `npx wrangler pages deploy` without pinning Wrangler to a specific trusted version. Because this command runs with Cloudflare API credentials in the environment, a malicious or compromised package version could abuse those credentials or alter deployment behavior.

Context-Inappropriate Capability

Low
Confidence
89% confidence
Finding
The manifest describes publishing selected markdown from a vault to a static site and deploying it, but this function additionally reads the user's Obsidian desktop configuration under the home directory to enumerate vault paths. That host-environment inspection is not necessary to perform publishing itself; it is only a convenience for setup and expands local access beyond the stated core purpose.

Static analysis

Detected: suspicious.dangerous_exec

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
bin/publishmd-cf.js:54