Back to skill

Security audit

Obsidian Cloudflare Pages

Security checks for vulnerabilities and agentic risk

Overview

The skill has a coherent publishing purpose, but its CLI uses unsafe shell commands, configurable destructive deletion, plaintext credential storage, and unpinned remote tooling that merit Review before installation.

Treat this as a Review item rather than a normal install. Only run it in a dedicated workspace with backed-up content, a narrowly scoped Cloudflare Pages token, and no valuable files reachable through configured publish paths. Avoid enabling Basic Auth until password handling is changed to use Cloudflare secrets or another managed secret store, and prefer a fixed, reviewed version of Quartz and Wrangler before deployment.

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
bin/publishmd-cf.js:40
Finding
Configuration-Controlled Shell Command Injection<![CDATA[ ## Vulnerability Details **File Location**: `bin/publishmd-cf.js`, lines 40–42, 258–266, and 472–484 **Vulnerability Type**: OS command injection through shell-string construction **Risk Level**: Critical ### Vulnerable Code ```js function sh(command, cwd, quiet = false) { if (quiet) return execSync(command, { stdio: ['ignore', 'pipe', 'pipe'], cwd: cwd || process.cwd() }).toString(); execSync(command, { stdio: 'inherit', cwd: cwd || process.cwd() }); } ``` ```js for (const folder of cfg.source.includeFolders) { const src = path.join(vaultPath, folder); if (!fs.existsSync(src)) { console.warn(`Skipping missing source folder: ${src}`); continue; } const excludes = (cfg.source.excludeFolders || []) .map((f) => `--exclude '${f}/'`) .join(' '); sh(`rsync -av --exclude '.obsidian/' --exclude '*.canvas' ${excludes} "${src}/" "${dest}/${folder}/"`); } ``` ```js const project = cfg.cloudflare.projectName; const branch = cfg.cloudflare.branch || 'main'; const tokenEnv = cfg.cloudflare?.apiTokenEnv || 'CLOUDFLARE_API_TOKEN'; const accountEnv = cfg.cloudflare?.accountIdEnv || 'CLOUDFLARE_ACCOUNT_ID'; const token = process.env[tokenEnv] || ''; const accountId = process.env[accountEnv] || ''; if (!token) throw new Error(`Missing Cloudflare token env var: ${tokenEnv}`); 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 sends complete command strings to `execSync`, which invokes a shell. Multiple values originating from editable configuration, wizard input, or environment variables are interpolated into these command strings. Shell quoting is incomplete and ...[truncated 2166 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Replace `execSync(commandString)` with `execFileSync` or `spawnSync`, using a fixed executable and a separate argument array. - Invoke `rsync` as an executable with arguments such as `['-av', '--exclude', value, source, destination]`; never concatenate configuration into a shell command. - Pass Cloudflare credentials through the child process `env` option rather than constructing an inline environment-variable prefix. - Apply strict allowlists to Cloudflare project names and branches where their expected syntax is known. - Reject folder and exclusion values containing null bytes, control characters, or unsupported path components. - Avoid enabling `shell: true`. - Add regression tests with quotes, semicolons, command substitutions, newlines, and backticks in every externally controlled field. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
bin/publishmd-cf.js:245
Finding
Unvalidated Paths Permit Directory Traversal and Destructive Deletion<![CDATA[ ## Vulnerability Details **File Location**: `bin/publishmd-cf.js`, lines 245–266 and 283–303 **Vulnerability Type**: Path traversal and unsafe recursive deletion **Risk Level**: High ### Vulnerable Code ```js 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 }); sh(`rm -rf "${dest}"/*`); for (const folder of cfg.source.includeFolders) { const src = path.join(vaultPath, folder); if (!fs.existsSync(src)) { console.warn(`Skipping missing source folder: ${src}`); continue; } const excludes = (cfg.source.excludeFolders || []) .map((f) => `--exclude '${f}/'`) .join(' '); sh(`rsync -av --exclude '.obsidian/' --exclude '*.canvas' ${excludes} "${src}/" "${dest}/${folder}/"`); } ``` ```js function setupProject() { const cfg = loadConfig(); const workspaceDir = expandHome(cfg.publish.workspaceDir); fs.mkdirSync(workspaceDir, { recursive: true }); const hasQuartz = fs.existsSync(path.join(workspaceDir, 'quartz.config.ts')); if (hasQuartz) { console.log('Quartz project already initialized ✅'); return; } console.log(`Initializing Quartz project in: ${workspaceDir}`); // 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 Q ...[truncated 2875 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Resolve and canonicalize all configured paths before use. - Enforce containment by checking that each destination path begins with the canonical workspace path plus the platform path separator. - Require included source folders to remain below the canonical vault root. - Reject absolute `contentDir` and folder values, `..` components, null bytes, and empty normalized paths. - Explicitly reject dangerous workspace targets such as filesystem roots, the user's home directory, the vault root, and the skill directory. - Create an ownership marker in workspaces initialized by this skill and refuse destructive cleanup unless the marker is present. - Replace shell-based `rm -rf` with validated filesystem APIs such as `fs.rmSync`, operating only on verified child entries. - Prompt for explicit confirmation before destructive fallback cleanup. - Prefer cloning into a new temporary directory and atomically moving it into place rather than clearing an existing directory. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
bin/publishmd-cf.js:438
Finding
Basic Authentication Credentials Are Echoed and Stored in Plaintext<![CDATA[ ## Vulnerability Details **File Location**: `bin/publishmd-cf.js`, lines 164–171, 183–199, and 438–451 **Vulnerability Type**: Plaintext sensitive-data storage and insecure password input **Risk Level**: Medium ### Vulnerable Code ```js 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 || ''); } ``` ```js const cfg = { source: { vaultPath: pickedVault, includeFolders: includeFolders.split(',').map((s) => s.trim()).filter(Boolean), excludeFolders: excludeFolders.split(',').map((s) => s.trim()).filter(Boolean), requireFrontmatterPublish: /^y(es)?$/i.test(requirePublish) }, publish: { workspaceDir, contentDir }, site: { generator: 'quartz', title, baseUrl, theme, showBacklinks: /^y(es)?$/i.test(showBacklinks), branding: { rootSourceFolder, clippingsIndexLabel, rootIndexLabel, sidebarTitleHtml } }, cloudflare: { projectName, branch, productionDomain: domain, apiTokenEnv, accountIdEnv, basicAuth: { enabled: /^y(es)?$/i.test(authEnabled), username: authUsername, password: authPassword } } }; saveConfig(cfg); ``` ```js if (!auth.username || !auth.password) { throw new Error('basicAuth is enabled but username/password is missing in config.cloudflare.basicAuth'); } fs.mkdirSync(fnDir, { recursive: true }); const middleware = `const USER = ${JSON.stringify(auth.username)};\nconst PASS = ${JSON.stringify(auth.password)};\n\nfunction unauthorized() {\n return new Response("Authentication required", {\n status: 401,\n headers: {\n "WWW-Au ...[truncated 2426 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Do not store the Basic Auth password in `config/config.json`. - Store only the name of a Cloudflare secret or environment binding in configuration. - Read the credential at runtime from `context.env` in the Pages Function. - Provision the value using Cloudflare's secret-management facilities rather than generated source code. - Implement hidden terminal input if a local interactive secret prompt remains necessary. - Write secret-related local files with restrictive permissions such as mode `0600`. - Ensure `config/config.json`, `.env`, and generated secret-bearing middleware are excluded from source control. - Rotate all credentials previously saved by the current implementation. - Document that Basic Auth must only be used over HTTPS and discourage reuse of the password. ]]>

T08 · Insecure Dependencies

Error
Location
bin/publishmd-cf.js:283
Finding
Unpinned Remote Dependencies and Repository Code Are Executed<![CDATA[ ## Vulnerability Details **File Location**: `bin/publishmd-cf.js`, lines 283–304, 380–382, and 472–484 **Vulnerability Type**: Unpinned third-party executable dependency and supply-chain exposure **Risk Level**: High ### Vulnerable Code ```js // 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) { sh(`rm -rf "${workspaceDir}"/*`); } sh(`git clone https://github.com/jackyzha0/quartz.git "${workspaceDir}"`); sh('npm i', workspaceDir); bootstrapOk = fs.existsSync(path.join(workspaceDir, 'quartz.config.ts')); } ``` ```js function build() { const cfg = loadConfig(); const workspaceDir = expandHome(cfg.publish.workspaceDir); sh('npx quartz build', workspaceDir); ``` ```js 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 setup, build, and deployment workflow executes third-party code without a fixed reviewed version: - `npx quartz create` does not specify a package version. - `git clone` retrieves the repository's current default branch rather than a reviewed commit. - `npm i` installs the dependency graph and may execute package lifecycle scripts. - `npx quartz build` and `n ...[truncated 1642 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Pin Quartz and Wrangler to exact reviewed versions. - Pin the Quartz repository fallback to a verified commit hash rather than the default branch. - Verify downloaded repository content or release artifacts against trusted hashes or signatures. - Commit a lockfile and use `npm ci` rather than unconstrained `npm i`. - Review and constrain npm lifecycle scripts; use `--ignore-scripts` where installation scripts are unnecessary. - Install dependencies during a controlled setup phase rather than allowing implicit `npx` downloads during build or deployment. - Run dependency installation and site generation in an isolated environment with minimal filesystem and network access. - Use a narrowly scoped Cloudflare token and expose it only to the final deployment process. - Add automated dependency auditing and an explicit update-review process. ]]>
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
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
Findings (25)

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The declared description understates the skill's behavior relative to the documented capabilities: local config discovery, home-directory reads, project cloning, package installation, HTML injection/branding changes, and basic-auth credential handling. This mismatch is dangerous because users may consent to a simple publishing action while the skill performs broader filesystem, network, and credential-affecting operations.

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
```
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
```
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
```
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
```
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
```
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
```bash
cp skills/obsidian-cloudflare-pages/.env.example skills/obsidian-cloudflare-pages/.env
# then edit .env
```

The CLI auto-loads `skills/obsidian-cloudflare-pages/.env` (without overriding existing shell env vars).
Confidence
74% confidence
Finding
The statement that the CLI auto-loads a skill-local `.env` means the tool will automatically ingest secrets from disk at runtime. In an agent environment, automatic secret loading increases the chance of unintended credential use during runs and makes secret access less explicit to the user.

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.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
if (!cfg.source.includeFolders?.length) throw new Error('includeFolders is empty');

  fs.mkdirSync(dest, { recursive: true });
  sh(`rm -rf "${dest}"/*`);

  for (const folder of cfg.source.includeFolders) {
    const src = path.join(vaultPath, folder);
Confidence
97% confidence
Finding
The script constructs a shell command `rm -rf "${dest}"/*` using configuration-derived path components and executes it via `execSync`. Even though the path is quoted, shell execution of destructive commands against configurable targets creates both severe data-loss risk and a tool-parameter-abuse primitive if path validation fails or assumptions about shell expansion break.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
if (!bootstrapOk) {
    console.log('Falling back to git clone bootstrap for Quartz...');
    if (fs.readdirSync(workspaceDir).length > 0) {
      sh(`rm -rf "${workspaceDir}"/*`);
    }
    sh(`git clone https://github.com/jackyzha0/quartz.git "${workspaceDir}"`);
    sh('npm i', workspaceDir);
Confidence
97% confidence
Finding
The fallback bootstrap path runs `rm -rf "${workspaceDir}"/*` through the shell using a user-configurable directory. This grants a highly destructive primitive to configuration input and can wipe arbitrary filesystem locations if the configured workspace is unsafe, mistyped, or adversarially supplied.

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
91% confidence
Finding
The skill requests or relies on environment-based capabilities for Cloudflare credentials but does not declare any explicit tool scope or permissions boundaries in the skill metadata. In an agent setting, undeclared capability use reduces transparency and can let the skill access secrets or runtime features that users did not clearly authorize.

Rp1

Medium
Category
MCP Rug Pull
Confidence
95% confidence
Finding
Using `npx quartz` without a pinned version allows retrieval of whatever package version is current at execution time, creating a supply-chain risk. A malicious or compromised upstream release could execute arbitrary code during bootstrap/build in the user's environment.

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
80% confidence
Finding
The skill encourages persistent storage of long-lived Cloudflare API credentials in shell profiles or local env files, which extends secret lifetime across sessions. If the host or agent context is later compromised, these persisted credentials could be reused to deploy or alter Cloudflare Pages and possibly DNS settings.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The sync step performs `rm -rf "${dest}"/*` before copying content, with no confirmation or safety guard beyond config-derived paths. If `workspaceDir` or `contentDir` is misconfigured, this can irreversibly delete arbitrary local files and is especially dangerous because the path is user-configurable and later passed to a shell command.

Rp1

Medium
Category
MCP Rug Pull
Confidence
87% confidence
Finding
The script executes `npx quartz create` without pinning a version or verifying package integrity, so the exact code fetched and run can change over time. Because this is part of project bootstrap, a compromised or malicious upstream package/version could execute arbitrary code on the operator's machine with the user's privileges.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
During fallback setup, the script wipes the entire configured workspace with `rm -rf` if the directory is non-empty, without explicit user approval. Because `workspaceDir` is configurable, a mistake or maliciously influenced config could cause broad data loss unrelated to the Quartz project.

Rp1

Medium
Category
MCP Rug Pull
Confidence
84% confidence
Finding
The build step runs `npx quartz build` without pinning the Quartz version, which allows execution of whatever version NPX resolves at runtime. In a publishing automation context, that creates a supply-chain execution path where upstream compromise or unexpected updates can run arbitrary code locally and alter published output.

Context-Inappropriate Capability

Medium
Confidence
86% confidence
Finding
The skill adds optional HTTP Basic Auth middleware and manages access credentials directly in its config/workspace, expanding scope from content publishing into access-control handling. That increases the attack surface and encourages storing secrets in less protected locations, which is risky in a publishing utility whose primary role does not require credential management logic.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The generated middleware embeds the basic-auth username and password directly into a file on disk, creating plaintext credential storage in the workspace. Those credentials may be exposed through backups, source control mistakes, local compromise, or accidental publication of project files.

Rp1

Medium
Category
MCP Rug Pull
Confidence
86% confidence
Finding
The deploy step invokes `npx wrangler pages deploy` without pinning Wrangler to a known version. Since this command runs with Cloudflare credentials in scope, an unexpected or malicious package version could exfiltrate tokens or deploy altered content.

Vague Triggers

Low
Confidence
84% confidence
Finding
The example prompt “Sync, build, and deploy to Cloudflare Pages.” describes a broad imperative rather than a narrowly scoped activation phrase or constrained invocation condition. In a skill description, this can blur when the skill should activate versus when a user is just discussing deployment steps.

Static analysis

Detected: suspicious.dangerous_exec

Shell command execution detected (child_process).

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