Back to skill

Security audit

Soul Search

Security checks for vulnerabilities and agentic risk

Overview

This skill does what it says, but it can replace a workspace's agent instructions with unverified remote content from sources broader than its safety rules describe.

Install only if you trust the skill publisher and the persona catalog sources, and avoid using it in sensitive workspaces unless you first review the persona content and source. Expect /soul apply and /soul restore to change SOUL.md and affect future sessions; keep an independent backup before applying personas.

Vulnerability Patterns
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • 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 (3)

T01 · Skill Instruction Hijacking

Error
Location
scripts/soul.mjs:373
Finding

Untrusted Remote Persona Content Is Installed as Agent Instructions

Content
View full analysis

Vulnerability Details

File Location: scripts/soul.mjs:7, 118-141, 205-215, 217-224, 373-395
Vulnerability Type: Remote agent-instruction payload installation
Risk Level: Critical

Vulnerable Code

js
const DEFAULT_CATALOG_URL = 'https://raw.githubusercontent.com/mergisi/awesome-openclaw-agents/refs/heads/main/agents.json';
js
export async function fetchRemoteText(url) {
  const parsed = parseCatalogUrl(url, 'URL');
  if (!parsed) {
    throw new Error(`Expected remote URL, got local path: ${url}`);
  }
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
  try {
    const res = await fetch(parsed.toString(), {
      headers: {
        'user-agent': USER_AGENT,
        'accept': 'application/json, text/plain;q=0.9, */*;q=0.8'
      },
      signal: controller.signal
    });
    if (!res.ok) throw new Error(`Fetch failed: ${res.status} ${res.statusText} for ${parsed}`);
    return await res.text();
  } catch (err) {
    if (err?.name === 'AbortError') {
      throw new Error(`Fetch timed out after ${FETCH_TIMEOUT_MS}ms for ${parsed}`);
    }
    throw err;
  } finally {
    clearTimeout(timeout);
  }
}
js
export function validateSoulContent(content, sourceUrl) {
  const text = typeof content === 'string' ? content.trim() : '';
  if (!text) throw new Error(`Fetched soul content is empty: ${sourceUrl}`);
  if (!(text.startsWith('#') || text.startsWith('---') || text.includes('\n# '))) {
    throw new Error(`Fetched content does not look like a SOUL.md file: ${sourceUrl}`);
  }
  return content;
}
js
if (subcommand === 'apply') {
  const id = rest[0] || '';
  if (!id) return print('Usage: soul apply <id>');
  const agent = findAgent(catalog, id);
  if (!agent) return print(`Soul not found: ${id}`);
  const sourceUrl = buildRawSoulUrl(state, agent);
  const content = validateSoulContent(await fetchText(sourceUrl), sourceUrl);
  const checksum = sha1(conten
...[truncated 3221 chars]
Remediation
View remediation

Remediation Suggestions

  1. Do not write remotely retrieved free-form text directly into a privileged agent instruction file.
  2. Pin the catalog and every persona to immutable commit hashes rather than mutable branches.
  3. Require a signed manifest containing each persona's canonical source, digest, version, and approved capabilities.
  4. Verify a modern cryptographic digest such as SHA-256 before installation. The expected digest must come from an authenticated source.
  5. Display the complete persona content and require explicit user confirmation before replacing SOUL.md.
  6. Parse personas through a restrictive schema and reject directives concerning secrets, tool execution, external communication, instruction precedence, policy bypass, or persistence.
  7. Prefer a constrained persona format containing only stylistic attributes, then generate trusted SOUL.md content locally.
  8. Record and expose immutable provenance, including repository, commit, signer, and verified digest.
  9. Add adversarial tests using Markdown-formatted prompt-injection payloads to confirm they are rejected.

T01 · Skill Instruction Hijacking

Error
Location
scripts/soul.mjs:205
Finding

Documented Remote Source Restriction Is Not Enforced

Content
View full analysis

Vulnerability Details

File Location: scripts/soul.mjs:205-215
Vulnerability Type: Unrestricted catalog-controlled remote source resolution
Risk Level: High

Vulnerable Code

js
export function buildRawSoulUrl(state, agent) {
  const relPath = normalizeRelativeSoulPath(agent.path || '');
  if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(relPath)) {
    return relPath;
  }
  if (/^(?:\.|\.|\/)/.test(relPath) || relPath.includes('/./') || relPath.includes('/../')) {
    return localWorkspacePath(relPath);
  }
  const parsed = parseCatalogUrl(state.catalogUrl, 'catalogUrl');
  const rawRoot = new URL('.', parsed);
  const resolved = new URL(relPath, rawRoot);
  return resolved.toString();
}

The resulting value is passed to the generic remote fetch function:

js
export async function fetchText(url) {
  const parsed = parseCatalogUrl(url, 'URL');
  return parsed ? fetchRemoteText(url) : readLocalText(url);
}

This conflicts with the documented rule in SKILL.md:37:

text
Only trust the configured catalog URL and raw GitHub content derived from its entries.

Technical Analysis

An agent path beginning with any syntactically valid URI scheme is returned unchanged. The code does not enforce:

  • The https: protocol
  • A raw GitHub hostname
  • An approved repository or path prefix
  • A default network port
  • The absence of embedded URL credentials
  • Validation of the final URL after redirects

As a result, a remotely controlled catalog entry can point to an arbitrary URL rather than content derived from the configured GitHub catalog location. The catalog therefore becomes an unrestricted redirection mechanism for persona payload retrieval.

For normal relative paths, new URL() resolves content relative to the catalog URL. However, absolute URL entries bypass that derivation entirely. The current tests verify successful GitHub URL construction but do not test rejection of arbitrary hosts, unsafe schemes, URL credentials, unusual ports, ...[truncated 1245 chars]

Remediation
View remediation

Remediation Suggestions

  1. Reject all absolute URLs in catalog path fields and allow only normalized relative paths.
  2. Resolve relative paths against a pinned, approved catalog root.
  3. Require https: and enforce an exact hostname allowlist.
  4. Enforce an approved repository and immutable commit path prefix.
  5. Reject URL usernames, passwords, non-default ports, fragments, backslashes, encoded traversal, and unexpected query parameters.
  6. Disable redirects where possible. Otherwise, validate every redirect target and the final response URL against the same allowlist.
  7. Normalize and decode paths before applying containment and repository-prefix checks.
  8. Add tests that reject arbitrary hosts, non-HTTPS schemes, embedded credentials, alternate ports, protocol-relative inputs, encoded traversal, and cross-origin redirects.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/soul.mjs:250
Finding

SOUL.md Replacement Continues When Backup Creation Fails

Content
View full analysis

Vulnerability Details

File Location: scripts/soul.mjs:250-259, 380-392
Vulnerability Type: Fail-open backup handling before destructive replacement
Risk Level: Medium

Vulnerable Code

js
export async function backupCurrentSoul() {
  try {
    const current = await fs.readFile(soulFile, 'utf8');
    const timestamp = new Date().toISOString().replaceAll(':', '-');
    const backupPath = path.join(backupDir, `SOUL-${timestamp}.md`);
    await writeAtomic(backupPath, current);
    return backupPath;
  } catch {
    return null;
  }
}
js
const backupPath = await backupCurrentSoul();
state.current = {
  id: agent.id,
  category: agent.category,
  name: agent.name || null,
  role: agent.role || null,
  sourceUrl,
  appliedAt: new Date().toISOString(),
  checksum,
  custom: false,
  agentId: agent.id
};
await Promise.all([writeAtomic(soulFile, content), saveState(state)]);

This conflicts with the safety guarantee in SKILL.md:38:

text
Never overwrite `SOUL.md` without first creating a backup in `soul-data/backups/`.

Technical Analysis

backupCurrentSoul() catches every error and converts it to null. This treats a missing original file identically to serious failures such as:

  • Permission denial while reading the existing file
  • Permission denial while writing the backup
  • Storage exhaustion
  • Filesystem or I/O errors
  • Failure during temporary-file creation or rename

The caller does not require a non-null backup before replacing SOUL.md. It uses the value only to decide whether the success message includes a backup path. Consequently, replacement is fail-open despite the documented invariant that a backup must exist first.

Attack Path

  1. A valid existing SOUL.md is present.
  2. Backup creation fails because of permissions, disk exhaustion, an I/O fault, or another filesystem error.
  3. The broad catch suppresses the reason and returns null.
  4. The apply flow continues and writes newly retrieved conte ...[truncated 622 chars]
Remediation
View remediation

Remediation Suggestions

  1. Abort apply if an existing SOUL.md cannot be read and successfully backed up.
  2. Catch only the expected ENOENT condition when no original file exists.
  3. Re-throw permission, storage, rename, and other I/O failures with actionable error messages.
  4. Verify that the backup exists and contains the expected bytes before replacing the original.
  5. Return a structured result such as { existed, backupPath } instead of using null for every failure condition.
  6. Perform backup, replacement, and state updates through a transaction-like sequence with rollback on failure.
  7. Add tests for permission errors, write failures, disk-full simulations, and existing-file backup failures.
  8. Ensure success output is emitted only after both required backup and replacement operations have completed.
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • 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
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (4)

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The README documents /soul apply and /soul restore as normal commands but does not clearly warn that they overwrite or restore SOUL.md in the current workspace. This can lead users to modify local files unintentionally, especially because the skill operates on remote persona content and workspace state, increasing the chance of accidental configuration changes or trust in unreviewed content.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding

The skill invokes a Node helper that, by its stated behavior, browses a remote catalog and fetches content, but the manifest does not declare any explicit tool scope such as permissions or allowed-tools. This creates an undeclared capability boundary: the skill can perform network-backed actions without transparent restriction, increasing the risk of unexpected remote content retrieval or expansion of behavior beyond what a reviewer or runtime policy expects.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

The notes explicitly state that remote SOUL.md files are applied into the current workspace, which implies local file modification based on untrusted remote content. Even though this file is documentation, the capability is risky because users may not be clearly warned that applying a persona changes local workspace state and could introduce adversarial instructions or overwrite expected files.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

This is a real vulnerability. buildRawSoulUrl() explicitly treats relative paths beginning with '.', '/', or containing path traversal markers as local workspace paths, and apply then reads that path and writes its contents into SOUL.md. Because agent.path is taken from a remotely fetched catalog, a catalog entry can cause the tool to exfiltrate or repurpose arbitrary workspace files (for example README, secrets accidentally stored as text, or other internal prompts/config) as the active persona, crossing the trust boundary between remote metadata and local files.

Content

No source excerpt is available for this finding.