Back to skill

Security audit

Frontend Doctor

Security checks for vulnerabilities and agentic risk

Overview

This frontend troubleshooting skill is mostly purpose-aligned, but its local scanner can read outside the intended project through symlinks and prints untrusted project text directly to the terminal.

Install only if you trust the projects you will scan. Avoid running it on hostile or unknown repositories until symlink boundary checks and terminal-output sanitization are added; it does not appear to send data over the network or persist background behavior.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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 (2)

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
src/scanner.js:20
Finding

Scan-Root Escape Through Symbolic Links

Content
View full analysis

Vulnerability Details

File Location: src/scanner.js:20-37
Vulnerability Type: Improper symbolic-link handling and scan-boundary bypass
Risk Level: Medium

Vulnerable Code

js
export function findFiles(dir, extensions, maxDepth = 4, depth = 0) {
  if (depth > maxDepth || !existsSync(dir)) return [];
  const results = [];
  try {
    for (const entry of readdirSync(dir)) {
      if (entry.startsWith('.') || entry === 'node_modules' || entry === 'dist' || entry === 'build') continue;
      const full = join(dir, entry);
      try {
        const stat = statSync(full);
        if (stat.isDirectory()) {
          results.push(...findFiles(full, extensions, maxDepth, depth + 1));
        } else if (extensions.includes(extname(entry))) {
          results.push(full);
        }
      } catch { /* skip inaccessible */ }
    }
  } catch { /* skip unreadable dir */ }
  return results;
}

Technical Analysis

statSync() follows symbolic links. The scanner does not use lstatSync() to detect links and does not compare each target's canonical path against the canonical scan root.

Consequently, a symbolic link inside a scanned project can reference a file or directory outside that project. If a file link has an accepted extension, or a directory link contains files with accepted extensions, findFiles() returns those external paths. Subsequent diagnostic functions call readText() or readJson() on them.

The maximum recursion depth limits traversal volume but does not enforce the intended filesystem boundary. The process remains constrained by the operating-system privileges of the user running the CLI.

Attack Path

  1. An attacker prepares an untrusted project containing a symbolic link to a readable external file or directory.
  2. The victim runs frontend-doctor against that project.
  3. readdirSync() discovers the link and statSync() follows it.
  4. The scanner treats the target as an ordinary file or directory.
  5. A diagnostic ...[truncated 643 chars]
Remediation
View remediation

Remediation Suggestions

  • Use lstatSync() or directory entries with explicit symbolic-link checks and reject links by default.
  • Canonicalize the initial scan root with realpathSync().
  • Canonicalize every candidate before reading or traversing it.
  • Verify that every candidate remains beneath the canonical root using path-segment-aware comparison rather than a raw string prefix.
  • Maintain a set of visited canonical directories to prevent cycles.
  • Apply the boundary check immediately before file reads as defense in depth and to reduce time-of-check/time-of-use exposure.
  • Add tests covering file links, directory links, links to parent directories, link cycles, and similarly prefixed sibling paths.

Example hardening approach:

js
import {
  existsSync,
  lstatSync,
  readdirSync,
  realpathSync,
} from 'fs';
import { relative, isAbsolute } from 'path';

function isWithinRoot(root, candidate) {
  const rel = relative(root, candidate);
  return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
}

export function findFiles(
  dir,
  extensions,
  maxDepth = 4,
  depth = 0,
  root = realpathSync(dir),
  visited = new Set()
) {
  if (depth > maxDepth || !existsSync(dir)) return [];

  const realDir = realpathSync(dir);
  if (!isWithinRoot(root, realDir) || visited.has(realDir)) return [];
  visited.add(realDir);

  const results = [];

  for (const entry of readdirSync(realDir)) {
    if (
      entry.startsWith('.') ||
      entry === 'node_modules' ||
      entry === 'dist' ||
      entry === 'build'
    ) continue;

    const full = join(realDir, entry);
    const stat = lstatSync(full);

    if (stat.isSymbolicLink()) continue;

    const realFull = realpathSync(full);
    if (!isWithinRoot(root, realFull)) continue;

    if (stat.isDirectory()) {
      results.push(
        ...findFiles(realFull, extensions, maxDepth, depth + 1, root, visited)
      );
    } else if (extensions.includes(extname(entry))) {
      results.push(realFull);
    }
  }
...[truncated 25 chars]

T09 · Insecure Skill Coding Practices

Warning
Location
src/report.js:1
Finding

Terminal Control-Sequence Injection Through Unsanitized Report Data

Content
View full analysis

Vulnerability Details

File Location: src/report.js:1-12
Related Sources: src/cli.js:11-13, src/scanner.js:58-67
Vulnerability Type: Terminal escape-sequence injection
Risk Level: Medium

Vulnerable Code

js
export function formatReport(results) {
  if (!results.length) {
    console.log('\n  \x1b[32m✓ No issues detected.\x1b[0m\n');
    return;
  }
  for (const r of results) {
    console.log(`\n  \x1b[31m✗ Root Cause:\x1b[0m  ${r.rootCause}`);
    console.log(`    \x1b[33mEvidence:\x1b[0m    ${r.evidence}`);
    console.log(`    \x1b[36mFix:\x1b[0m         ${r.fix}`);
  }
  console.log();
}

Untrusted values reach this output through the scan path and matching source lines:

js
const cwd = resolve(process.argv[2] || '.');

console.log(`\n  \x1b[1mfrontend-doctor\x1b[0m  scanning ${cwd} ...\n`);
js
export function grepFiles(files, pattern) {
  const matches = [];
  for (const file of files) {
    const content = readText(file);
    if (!content) continue;
    const lines = content.split('\n');
    for (let i = 0; i < lines.length; i++) {
      if (pattern.test(lines[i])) {
        matches.push({ file, line: i + 1, text: lines[i].trim() });
      }
    }
  }
  return matches;
}

Technical Analysis

The CLI intentionally emits its own ANSI formatting but interpolates the scan path, filenames, and matching source text without sanitizing terminal control characters. These values can be controlled by the contents and names of files in an untrusted project.

An attacker can embed ESC, CSI, OSC, or other control characters in a filename, directory name, or source line that matches one of the diagnostic expressions. When the value reaches console.log(), an ANSI-capable terminal can interpret it as terminal instructions rather than displaying it as inert text.

Depending on terminal behavior and configuration, this can clear or rewrite visible output, conceal genuine findings, create misleading report text, alte ...[truncated 1121 chars]

Remediation
View remediation

Remediation Suggestions

  • Sanitize all values derived from command-line arguments, filesystem names, and scanned file contents before terminal output.
  • Strip or visibly encode ESC, C0/C1 control characters, DEL, CSI sequences, and OSC sequences.
  • Preserve only the CLI's own trusted formatting sequences.
  • Provide a plain-output or structured JSON mode for automation.
  • Truncate exceptionally long evidence lines to reduce terminal abuse and denial-of-service risks.
  • Add tests containing ESC, CSI, OSC 8, OSC 52, carriage return, backspace, and newline characters.

Example hardening:

js
function sanitizeTerminal(value) {
  return String(value)
    // OSC sequences
    .replace(/\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)/g, '')
    // CSI and related ANSI sequences
    .replace(/\x1B(?:[@-_]|\[[0-?]*[ -/]*[@-~])/g, '')
    // Remaining control characters except tab
    .replace(/[\x00-\x08\x0A-\x1F\x7F-\x9F]/g, char =>
      `\\x${char.charCodeAt(0).toString(16).padStart(2, '0')}`
    );
}

export function formatReport(results) {
  if (!results.length) {
    console.log('\n  \x1b[32m✓ No issues detected.\x1b[0m\n');
    return;
  }

  for (const r of results) {
    console.log(
      `\n  \x1b[31m✗ Root Cause:\x1b[0m  ${sanitizeTerminal(r.rootCause)}`
    );
    console.log(
      `    \x1b[33mEvidence:\x1b[0m    ${sanitizeTerminal(r.evidence)}`
    );
    console.log(
      `    \x1b[36mFix:\x1b[0m         ${sanitizeTerminal(r.fix)}`
    );
  }

  console.log();
}

The same sanitization must be applied to cwd before the scan banner is printed.

Vulnerability Patterns
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
Findings (7)

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · src/diagnostics/white-screen.js (reported line 25)May include surrounding context.

js
}
  }

  // Check: env vars referenced but .env missing
  const envFile = join(cwd, '.env');
  const envLocalFile = join(cwd, '.env.local');
  const envPatterns = {

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · src/diagnostics/white-screen.js (reported line 37)May include surrounding context.

js
}
  }

  // Check: env vars referenced but .env missing
  const envFile = join(cwd, '.env');
  const envLocalFile = join(cwd, '.env.local');
  const envPatterns = {

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · src/diagnostics/white-screen.js (reported line 39)May include surrounding context.

js
}
  }

  // Check: env vars referenced but .env missing
  const envFile = join(cwd, '.env');
  const envLocalFile = join(cwd, '.env.local');
  const envPatterns = {

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · src/diagnostics/white-screen.js (reported line 26)May include surrounding context.

js
}

  // Check: env vars referenced but .env missing
  const envFile = join(cwd, '.env');
  const envLocalFile = join(cwd, '.env.local');
  const envPatterns = {
    vite: /import\.meta\.env\.\w+/,

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · src/diagnostics/white-screen.js (reported line 27)May include surrounding context.

js
// Check: env vars referenced but .env missing
  const envFile = join(cwd, '.env');
  const envLocalFile = join(cwd, '.env.local');
  const envPatterns = {
    vite: /import\.meta\.env\.\w+/,
    next: /process\.env\.NEXT_PUBLIC_\w+/,

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

The README shows a single natural-language invocation example for the skill but does not specify whether activation is limited to the explicit command form, what variants are accepted, or any non-triggering examples. This makes the activation scope unclear and could lead to unintended invocation from broadly phrased frontend troubleshooting requests.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
70% confidence
Finding

Without declared permissions the skill's intent is opaque and cannot be validated.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.