Back to skill

Security audit

Agent Task Queue

Security checks for vulnerabilities and agentic risk

Overview

This is a coherent task-queue skill, but its distributed-worker implementation can duplicate or overwrite queued work in ways users should review before relying on it.

Review this skill carefully before using it for distributed or high-impact jobs. It is most appropriate for local testing or workflows where handlers are idempotent and duplicate execution is tolerable. If using Redis or multiple workers, add atomic task claiming, duplicate-ID rejection, Redis access controls, and clear handling for sensitive task payloads and logs.

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

T09 · Insecure Skill Coding Practices

Error
Location
src/TaskQueue.ts:41
Finding
Non-Atomic Task Claiming Allows Duplicate Distributed Execution<![CDATA[ ## Vulnerability Details **File Location**: `src/TaskQueue.ts:41-56`; affected persistence operations also appear in `src/storage/RedisStorage.ts:41-49` and `src/storage/SQLiteStorage.ts:45-52` **Vulnerability Type**: Race condition / non-atomic distributed task claim **Risk Level**: High ### Vulnerable Code ```ts async claimNextReady(): Promise<TaskRecord | undefined> { const tasks = await this.storage.listTasks(); const now = Date.now(); const ready = tasks .filter((task) => ["queued", "retry_scheduled"].includes(task.status)) .filter((task) => new Date(task.runAt).getTime() <= now) .sort(compareReadyTasks); const next = ready[0]; if (!next) { return undefined; } next.status = "running"; next.attempts += 1; next.startedAt = new Date().toISOString(); next.updatedAt = next.startedAt; await this.storage.updateTask(next); await this.log(next.id, "info", "Task claimed for execution", { attempt: next.attempts }); return next; } ``` The Redis backend performs independent reads and writes: ```ts async updateTask(task: TaskRecord): Promise<void> { await this.saveTask(task); } async listTasks(): Promise<TaskRecord[]> { await this.connect(); const entries = await this.client.hVals(this.key("tasks")); return entries.map((entry) => JSON.parse(entry) as TaskRecord); } ``` The SQLite backend likewise separates listing from updating: ```ts async updateTask(task: TaskRecord): Promise<void> { await this.saveTask(task); } async listTasks(): Promise<TaskRecord[]> { const rows = this.db.prepare("SELECT id, data FROM tasks").all() as Record<string, unknown>[]; return rows.map(parseTask); } ``` ### Technical Analysis Task claiming is implemented as a read-select-write sequence rather than an atomic storage operation. Each scheduler first obtains all task records, selects the highest-priority ready task locally, and only afterward writes the updated `running` state. When two schedulers poll the same Redis or ...[truncated 2089 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Move claiming into the `QueueStorage` interface as a dedicated atomic operation, such as: ```ts claimNextReady(now: Date, workerId: string): Promise<TaskRecord | undefined>; ``` 2. For Redis, implement the selection and state transition in one Lua script or equivalent atomic transaction. The script should: - Select only an eligible queued task. - Atomically change its state to `running`. - Assign an unpredictable claim or lease token. - Record the worker identity and lease expiration. - Return the task only when the claim succeeds. 3. For SQLite, use a transaction and conditional update. The update must include an eligibility predicate and verify that exactly one row changed, for example: ```sql UPDATE tasks SET data = ? WHERE id = ? AND current_status IN ('queued', 'retry_scheduled'); ``` Consider storing status and scheduling fields in dedicated columns rather than only inside JSON so they can be selected and updated safely in SQL. 4. Require the claim token or matching worker ownership for `complete()` and `fail()` operations. This prevents a stale worker from overwriting the result after its lease expires. 5. Add lease renewal and recovery for workers that crash while holding a task. 6. Add concurrency tests using multiple queue and scheduler instances against the same Redis and SQLite storage, asserting that a task handler executes exactly once. 7. Document whether handlers must be idempotent. Even with atomic claiming, idempotency keys should be used for sensitive external side effects. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
src/TaskQueue.ts:24
Finding
Caller-Controlled Duplicate Task IDs Silently Overwrite Existing Tasks<![CDATA[ ## Vulnerability Details **File Location**: `src/TaskQueue.ts:24-34`; overwrite behavior occurs in `src/storage/InMemoryStorage.ts:8-10`, `src/storage/SQLiteStorage.ts:35-37`, and `src/storage/RedisStorage.ts:29-32` **Vulnerability Type**: Task record overwrite / missing uniqueness enforcement **Risk Level**: Medium ### Vulnerable Code The enqueue path does not check whether the supplied task ID already exists: ```ts async enqueue<TPayload>(options: TaskOptions<TPayload>): Promise<TaskRecord<TPayload>> { const task = createTaskRecord(options); await this.dependencies.validateTask(task); if (task.status === "waiting_dependencies" && (await this.dependencies.areDependenciesSatisfied(task))) { await this.dependencies.persistDependencyResults(task); task.status = new Date(task.runAt).getTime() <= Date.now() ? "queued" : "retry_scheduled"; } await this.storage.saveTask(task); await this.log(task.id, "info", "Task enqueued", { priority: task.priority, runAt: task.runAt, dependencies: task.dependencies }); return task; } ``` Every storage backend then replaces the existing value. In-memory storage: ```ts async saveTask(task: TaskRecord): Promise<void> { this.tasks.set(task.id, structuredClone(task)); } ``` SQLite storage: ```ts async saveTask(task: TaskRecord): Promise<void> { this.db.prepare("INSERT OR REPLACE INTO tasks (id, data) VALUES (?, ?)").run(task.id, JSON.stringify(task)); } ``` Redis storage: ```ts async saveTask(task: TaskRecord): Promise<void> { await this.connect(); await this.client.hSet(this.key("tasks"), task.id, JSON.stringify(task)); } ``` ### Technical Analysis `TaskOptions.id` is caller-controlled. The queue creates a random UUID only when the caller does not supply an ID. When a supplied ID already exists, `enqueue()` performs no duplicate check and all three backends silently overwrite the existing record. This violates the expected distinction between creating a task and updatin ...[truncated 2120 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Make task creation insert-only and reject duplicate IDs before accepting a task. 2. Enforce uniqueness atomically in each backend rather than relying only on a preliminary `getTask()` check: - In memory: check `tasks.has(task.id)` before insertion. - SQLite: use plain `INSERT`, not `INSERT OR REPLACE`, and handle the unique-constraint error. - Redis: use `HSETNX` or an atomic Lua script and reject the enqueue operation when the field already exists. 3. Do not use a separate read followed by a write as the only duplicate defense, because concurrent producers can pass the read check simultaneously. 4. Separate creation from mutation: - `saveNewTask()` should fail if the ID exists. - `updateTask()` should require the expected record version and legal state transition. - Administrative replacement, if needed, should be a distinct privileged operation. 5. Add an immutable owner or tenant identifier to each task and enforce authorization for reads, updates, cancellation, completion, and failure operations. 6. Add optimistic concurrency control, such as a monotonically increasing version field, to prevent stale workers from overwriting newer state. 7. Define whether task IDs are confidential or public. Do not treat ID unpredictability as the primary authorization mechanism. 8. Add tests covering duplicate enqueue attempts for every backend, including simultaneous attempts from multiple producers, and assert that the original task remains unchanged. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
Findings (8)

Missing User Warnings

Medium
Confidence
80% confidence
Finding
This code opens a Redis connection and sends serialized task records, logs, and dependency results over the network via Redis commands such as hSet, hGet, hVals, lRange, rPush, and hGetAll. There are no comments, docstrings, logging statements, or confirmation prompts in the file disclosing that user or system data is being transmitted to an external Redis service.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"node": ">=20"
  },
  "dependencies": {
    "better-sqlite3": "^11.9.0",
    "redis": "^5.1.0"
  },
  "devDependencies": {
Confidence
88% confidence
Finding
The production dependency better-sqlite3 is specified with a caret range, which allows automatic installation of newer minor/patch releases. This weakens build reproducibility and can expose consumers to supply-chain risk or unintended dependency changes if a compromised or breaking release is published within the allowed range.

Unpinned Dependencies

Low
Category
Supply Chain
Content
},
  "dependencies": {
    "better-sqlite3": "^11.9.0",
    "redis": "^5.1.0"
  },
  "devDependencies": {
    "@types/better-sqlite3": "^7.6.13",
Confidence
91% confidence
Finding
The redis runtime dependency is unpinned via a caret range, so different installations may resolve to different package versions over time. In a task-queue skill that likely relies on Redis for coordination, this increases supply-chain exposure and may introduce vulnerable or incompatible releases without an explicit code change.

Unverifiable Dependency: redis has 1 known advisory(ies) (CVE-2021-29469 (Node-Redis potential exponential regex in monitor mode)), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
40% confidence
Finding
Dependency has known vulnerabilities (CVEs). Using packages with unpatched security flaws exposes the environment to known exploits.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"redis": "^5.1.0"
  },
  "devDependencies": {
    "@types/better-sqlite3": "^7.6.13",
    "@types/node": "^22.13.10",
    "tsx": "^4.19.3",
    "typescript": "^5.8.2"
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Unpinned Dependencies

Low
Category
Supply Chain
Content
},
  "devDependencies": {
    "@types/better-sqlite3": "^7.6.13",
    "@types/node": "^22.13.10",
    "tsx": "^4.19.3",
    "typescript": "^5.8.2"
  }
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"devDependencies": {
    "@types/better-sqlite3": "^7.6.13",
    "@types/node": "^22.13.10",
    "tsx": "^4.19.3",
    "typescript": "^5.8.2"
  }
}
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"@types/better-sqlite3": "^7.6.13",
    "@types/node": "^22.13.10",
    "tsx": "^4.19.3",
    "typescript": "^5.8.2"
  }
}
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.