Back to skill

Security audit

Workflow Engine

Security checks for vulnerabilities and agentic risk

Overview

This workflow-engine skill is not malicious, but it has serious file-writing and workflow-control bugs that users should review before installing.

Review this package carefully before installing it in any environment that handles secrets, personal data, approvals, payments, deployments, deletion, or other side effects. Disable or isolate checkpointing unless you understand what data will be written, validate execution IDs before exposing them to users, and do not rely on the conditional helper for security-sensitive branching until the branch execution bug is fixed.

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

T09 · Insecure Skill Coding Practices

Error
Location
src/executor.js:164
Finding

Caller-Controlled Execution ID Enables Checkpoint Path Traversal

Content
View full analysis

Vulnerability Details

File Location: src/executor.js:31, src/executor.js:164-190
Vulnerability Type: Path traversal allowing unauthorized JSON file access and overwrite
Risk Level: High

Vulnerable Code

javascript
async execute(dag, context = {}, executionId = null) {
  executionId = executionId || `exec_${Date.now()}`;
  this.emit('execution-started', { executionId, dag: dag.id });

  // Check checkpoint
  let state = await this.loadCheckpoint(executionId);
javascript
async saveCheckpoint(state) {
  try {
    await fs.mkdir(this.checkpointDir, { recursive: true });
    const checkpointPath = path.join(this.checkpointDir, `${state.executionId}.json`);
    const checkpoint = {
      ...state,
      completedNodes: Array.from(state.completedNodes),
      failedNodes: Array.from(state.failedNodes),
      nodeResults: Array.from(state.nodeResults.entries())
    };
    await fs.writeFile(checkpointPath, JSON.stringify(checkpoint), 'utf-8');
  } catch (error) {
    console.warn('Checkpoint save failed:', error);
  }
}

async loadCheckpoint(executionId) {
  try {
    const checkpointPath = path.join(this.checkpointDir, `${executionId}.json`);
    const content = await fs.readFile(checkpointPath, 'utf-8');
    const checkpoint = JSON.parse(content);
    return {
      ...checkpoint,
      completedNodes: new Set(checkpoint.completedNodes),
      failedNodes: new Set(checkpoint.failedNodes),
      nodeResults: new Map(checkpoint.nodeResults)
    };
  } catch {
    return null;
  }
}

Technical Analysis

The public execution API accepts an externally supplied executionId and passes it directly to loadCheckpoint(). The same identifier is subsequently used by saveCheckpoint().

Appending .json does not neutralize directory traversal sequences. For example, joining the default checkpoint directory with an execution ID such as ../../data/target resolves outside the intended checkpoint directory. No allowlist validation, ca ...[truncated 1679 chars]

Remediation
View remediation

Remediation Suggestions

  1. Do not accept filesystem identifiers directly from untrusted callers. Generate execution IDs internally using crypto.randomUUID().
  2. If caller-supplied IDs are required, enforce a strict allowlist:
javascript
const EXECUTION_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;

function validateExecutionId(executionId) {
  if (!EXECUTION_ID_PATTERN.test(executionId)) {
    throw new Error('Invalid execution ID');
  }
}
  1. Resolve and verify the resulting path before every read or write:
javascript
const checkpointRoot = path.resolve(this.checkpointDir);
const checkpointPath = path.resolve(checkpointRoot, `${executionId}.json`);

if (
  checkpointPath === checkpointRoot ||
  !checkpointPath.startsWith(`${checkpointRoot}${path.sep}`)
) {
  throw new Error('Checkpoint path escapes the configured directory');
}
  1. Reject absolute paths, path separators, traversal components, null bytes, and excessively long identifiers.
  2. Write checkpoints atomically through a temporary file in the validated directory, then rename it.
  3. Do not silently suppress security-relevant filesystem errors. Emit structured audit events without exposing sensitive contents.
  4. Add tests covering ../, absolute paths, platform-specific separators, encoded separators, and symlink-related escape attempts.

T09 · Insecure Skill Coding Practices

Warning
Location
src/executor.js:164
Finding

Workflow Context and Node Results Are Persisted in Plaintext by Default

Content
View full analysis

Vulnerability Details

File Location: src/executor.js:43-51, src/executor.js:164-174
Vulnerability Type: Plaintext persistence of potentially sensitive workflow data
Risk Level: Medium

Vulnerable Code

javascript
state = {
  executionId,
  dagId: dag.id,
  status: 'running',
  context: { ...context },
  completedNodes: new Set(),
  failedNodes: new Set(),
  nodeResults: new Map(),
  startTime: Date.now(),
  endTime: null
};
javascript
async saveCheckpoint(state) {
  try {
    await fs.mkdir(this.checkpointDir, { recursive: true });
    const checkpointPath = path.join(this.checkpointDir, `${state.executionId}.json`);
    const checkpoint = {
      ...state,
      completedNodes: Array.from(state.completedNodes),
      failedNodes: Array.from(state.failedNodes),
      nodeResults: Array.from(state.nodeResults.entries())
    };
    await fs.writeFile(checkpointPath, JSON.stringify(checkpoint), 'utf-8');
  } catch (error) {
    console.warn('Checkpoint save failed:', error);
  }
}

Technical Analysis

Checkpointing is enabled unless explicitly disabled. The checkpoint object spreads the complete execution state, including the full workflow context, and serializes all node results. No field filtering, secret redaction, encryption, retention policy, or explicit restrictive file mode is applied.

The reviewed artifact confirms that this behavior produces persistent records. For example, checkpoints/exec_1774506672072.json:1 contains the full user array and processed workflow data:

json
{"context":{"data":{"users":["Alice","Bob","Charlie"],"processed":["ALICE","BOB","CHARLIE"]}}}

While the sample data is not itself sensitive, real workflow contexts commonly contain API tokens, credentials, personal information, access decisions, or proprietary records. Such values would be persisted in the same manner.

Attack Path

  1. A caller supplies sensitive data in the workflow context, or a task returns sensitive data in ...[truncated 1122 chars]
Remediation
View remediation

Remediation Suggestions

  1. Make checkpoint persistence opt-in rather than enabled by default for workflows that may process sensitive data.
  2. Introduce an explicit checkpoint schema that stores only the minimum state needed for resumption.
  3. Provide configurable redaction for sensitive fields such as tokens, passwords, authorization headers, cookies, and personal data.
  4. Never persist secrets merely because they are present in task context or results.
  5. Create checkpoint files with restrictive permissions:
javascript
await fs.writeFile(
  checkpointPath,
  JSON.stringify(checkpoint),
  { encoding: 'utf-8', mode: 0o600 }
);
  1. Encrypt checkpoints at rest when sensitive state must be recoverable, with keys stored separately from checkpoint files.
  2. Define expiration and deletion policies for completed and failed executions.
  3. Add checkpoints/ to .gitignore and package exclusion rules unless sanitized fixtures are intentionally required.
  4. Document that task authors must not return credentials or secrets unless a secure persistence policy is configured.
  5. Add automated tests verifying that configured sensitive fields never appear in checkpoint output.

T09 · Insecure Skill Coding Practices

Error
Location
src/workflow-engine.js:96
Finding

Conditional Workflow Executes Both Branches Regardless of the Condition Result

Content
View full analysis

Vulnerability Details

File Location: src/workflow-engine.js:96-120, src/executor.js:141-146
Vulnerability Type: Broken workflow control flow and authorization bypass
Risk Level: High

Vulnerable Code

javascript
createConditionalWorkflow(name, config) {
  const dag = this.createWorkflow({ name });

  dag.addNode(new WorkflowNode({ id: 'start', type: 'start', name: 'Start' }));

  dag.addNode(new WorkflowNode({
    id: 'condition',
    type: 'condition',
    name: 'Condition',
    condition: config.condition,
    branches: {
      'true': 'task_true',
      'false': 'task_false'
    }
  }));

  dag.addNode(new WorkflowNode({
    id: 'task_true',
    type: 'task',
    name: 'True Branch',
    execute: config.trueBranch
  }));

  dag.addNode(new WorkflowNode({
    id: 'task_false',
    type: 'task',
    name: 'False Branch',
    execute: config.falseBranch
  }));

  dag.addNode(new WorkflowNode({ id: 'end', type: 'end', name: 'End' }));

  dag.addEdge('start', 'condition');
  dag.addEdge('task_true', 'end');
  dag.addEdge('task_false', 'end');

  return dag;
}
javascript
async executeConditionNode(node, state) {
  const condition = await node.condition(state.context, state);
  const branch = condition ? 'true' : 'false';
  return { condition, branch, nextNode: node.branches[branch] };
}

Technical Analysis

The condition callback calculates a selected branch and returns its node ID, but the scheduler never consumes nextNode to control which nodes are eligible for execution.

In the generated DAG, neither task_true nor task_false has an incoming edge from the condition node. Both therefore have zero dependencies and are treated as runnable by the topological execution-level algorithm. The selected branch is recorded as data only; it has no control-flow effect.

The persisted artifact checkpoints/exec_1774578175333.json:1 confirms this behavior. It records a true condition while both branch nodes are completed:

j
...[truncated 1616 chars]
Remediation
View remediation

Remediation Suggestions

  1. Represent conditional branches as actual outgoing edges from the condition node:
javascript
dag.addEdge('start', 'condition');
dag.addEdge('condition', 'task_true');
dag.addEdge('condition', 'task_false');
  1. Extend the scheduler to activate only the edge matching the evaluated condition.
  2. Explicitly mark the unselected branch as skipped so downstream dependency handling can distinguish skipped nodes from successful nodes.
  3. Ensure merge nodes can proceed according to clearly defined branch semantics rather than requiring both mutually exclusive branches.
  4. Validate that every branch target exists and is reachable from its condition node.
  5. Prevent a condition result from selecting arbitrary nodes outside its declared branch mapping.
  6. Add security regression tests that:
    • Assert exactly one branch callback runs.
    • Assert the unselected callback has no side effects.
    • Cover true, false, rejected, and timed-out conditions.
    • Cover nested conditions and branch merge behavior.
  7. Until branch-aware scheduling is implemented, do not use this mechanism for authorization or other security-sensitive decisions.

T09 · Insecure Skill Coding Practices

Warning
Location
src/executor.js:58
Finding

Configured Concurrency Limit Is Ignored, Allowing Resource Exhaustion

Content
View full analysis

Vulnerability Details

File Location: src/executor.js:18, src/executor.js:58-60, src/executor.js:149-152
Vulnerability Type: Unbounded concurrent task execution leading to denial of service
Risk Level: Medium

Vulnerable Code

javascript
this.maxConcurrency = config.maxConcurrency || 10;
javascript
for (const level of levels) {
  const promises = level.map(nodeId => this.executeNode(dag, nodeId, state));
  const results = await Promise.allSettled(promises);

  const failures = results.filter(r => r.status === 'rejected');
  if (failures.length > 0) {
    state.status = 'failed';
    break;
  }
}
javascript
async executeParallelNode(node, state) {
  const tasks = node.parallelTasks.map(task => task.execute(state.context, state));
  const results = await Promise.allSettled(tasks);
  return { results };
}

Technical Analysis

Although the constructor accepts and stores maxConcurrency, that value is never enforced. Every node in an execution level is started immediately through Array.map(), and every task within a parallel node is also started immediately.

Promise.allSettled() waits for all operations but does not constrain how many have already begun. Therefore, a workflow containing a large number of independent nodes or parallel tasks can create an arbitrarily large number of simultaneous asynchronous operations.

Task timeouts do not resolve the issue because the timeout rejects only the wrapper promise. It does not cancel the underlying callback or release resources held by operations that ignore cancellation.

Attack Path

  1. An attacker, tenant, plugin, or untrusted workflow author registers a DAG containing many independent nodes or a parallel node with many tasks.
  2. The workflow is invoked.
  3. The executor maps over every runnable node or parallel task without consulting maxConcurrency.
  4. All callbacks begin concurrently.
  5. The callbacks consume memory, CPU, sockets, file descriptors, datab ...[truncated 819 chars]
Remediation
View remediation

Remediation Suggestions

  1. Enforce maxConcurrency with a shared semaphore or bounded worker queue.
  2. Apply the same limiter to:
    • Nodes within an execution level.
    • Tasks inside parallel nodes.
    • Concurrent workflow executions if they share process resources.
  3. Validate maxConcurrency as a finite positive integer and impose a safe upper bound.
  4. Add maximum node-count and parallel-task-count limits for externally supplied workflows.
  5. Introduce cancellation through AbortController and require task implementations to honor the provided signal.
  6. Add per-workflow and global resource quotas, including execution count, duration, memory-sensitive workload size, and downstream request budgets.
  7. Consider a bounded implementation such as:
javascript
async function runBounded(items, limit, worker) {
  const queue = [...items];
  const workers = Array.from(
    { length: Math.min(limit, queue.length) },
    async () => {
      while (queue.length > 0) {
        const item = queue.shift();
        await worker(item);
      }
    }
  );

  await Promise.allSettled(workers);
}
  1. Add stress tests proving that active callbacks never exceed the configured concurrency limit.
  2. Monitor active workflows, pending tasks, task duration, memory use, and rejected downstream operations.
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • 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
Findings (10)

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
import WorkflowDAG, { WorkflowNode } from './src/dag.js';

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The documentation advertises state persistence and checkpointing but does not warn users that workflow context and execution state may be written to disk, potentially including sensitive input, intermediate results, tokens, or personal data. In a workflow engine, persisted state is a realistic security and privacy concern because users may enable checkpointing by default without understanding the storage implications.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
95% confidence
Finding

The entire skill documentation is presented in Chinese, with no indication that users may choose another language or that the skill is intentionally limited to a Chinese-speaking audience. This creates a locale/language policy concern because the documentation effectively imposes one language without opt-in or documented justification.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
84% confidence
Finding

This code file contains user-facing descriptive text and comments in Chinese (for example the title at L02 and comment at L12) without offering any language choice or documenting a justified locale restriction. Under the policy rule, forcing a specific language or locale without opt-in can be a natural-language policy violation.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
94% confidence
Finding

The package description is written entirely in Chinese, which can impose a language-specific experience without offering any user choice or documenting that the skill is intended only for a Chinese-speaking or region-specific context. This matches the language/locale policy concern for natural-language content in config files.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
90% confidence
Finding

This JavaScript file uses Chinese-language natural-language comments throughout, beginning with the module description, without any indication that the skill is region-specific or that users can opt into this locale. Under the policy, forcing a specific language without user choice or documented justification is a natural-language policy concern.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
88% confidence
Finding

This code file uses Chinese in prominent natural-language comments describing the engine and executor, with no indication that the skill supports other languages or that Chinese is required for a region-specific purpose. Under the policy rule, forcing a specific language without user opt-in is a natural-language policy concern.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
88% confidence
Finding

This code file contains natural-language descriptions such as '统一入口' and other Chinese-only comments/documentation, which may impose a language expectation without user opt-in. The policy calls for flagging language or locale constraints when the skill does not offer a choice or clearly justify the restriction.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
88% confidence
Finding

This JavaScript test file contains natural-language strings and comments in Chinese, such as the file header and test-runner comment, without indicating that the skill is region-specific or that users may choose another language. The policy scope includes all file types, and fixed locale/language usage without opt-in can violate organizational language-choice requirements.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
80% confidence
Finding

The natural-language content around test execution includes Chinese text, and the file does not state that this is a locale-specific tool or provide an alternative language option. Because SQP-3 applies to natural-language strings in all file types, fixed-language output or messaging can be a policy issue when no user choice is offered.

Content

No source excerpt is available for this finding.