Back to skill

Security audit

stream-formatter

Security checks for vulnerabilities and agentic risk

Overview

The skill is a plausible stream formatter, but it stores streamed text in shared module memory without stream or user isolation, which could mix or expose conversations in a shared runtime.

Install only if the skill will run in a strictly isolated per-user or per-conversation process, or after it is changed to keep per-stream state, enforce chunk and buffer limits, expire/reset abandoned streams, and bind reset/configuration to the correct caller. The Chinese-only documentation and remote zod import should also be reviewed for fit with your environment.

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
index.ts:26
Finding

Shared Global State Enables Cross-Session Data Leakage and Stream Corruption

Content
View full analysis

Vulnerability Details

File Location: index.ts, lines 26-34 and 54-75
Vulnerability Type: Shared mutable state without session isolation
Risk Level: High

Vulnerable Code

typescript
// Global state
let buffer = "";
let lastOutput = "";
let config = {
  buffer_size: 10,
  format_markdown: true,
  fix_incomplete_sentences: true,
  remove_duplicates: true
};
typescript
export default async function streamFormatter(params: StreamFormatterParams) {
  const validatedParams = StreamFormatterParams.parse(params);

  switch (validatedParams.action) {
    case "init": {
      config = { ...config, ...validatedParams.options };
      buffer = "";
      lastOutput = "";
      return { success: true, config };
    }

    case "process": {
      const { chunk, flush } = validatedParams;

      // Add to buffer
      buffer += chunk;

      // Duplicate removal
      if (config.remove_duplicates && buffer.includes(lastOutput) && lastOutput.length > 0) {
        buffer = buffer.replace(lastOutput, "");
      }

Technical Analysis

The formatter stores its buffer, previous output, and configuration in module-level mutable variables. Every invocation handled by the same runtime therefore operates on the same state. The API does not accept a stream identifier, session identifier, or caller-specific state object.

Calling init resets the shared state and changes the shared configuration. Calling process appends data to the same shared buffer, regardless of which user or conversation supplied it. In a persistent or concurrent multi-user runtime, one conversation can consequently read, modify, flush, reset, or corrupt another conversation's pending output.

Asynchronous function syntax does not provide isolation. Interleaved calls can access this state between separate requests, even though the function contains no explicit asynchronous operation a ...[truncated 1459 chars]

Remediation
View remediation

Remediation Suggestions

  • Remove module-global mutable stream state.
  • Create a separate formatter instance for each stream and keep buffer, lastOutput, and config in that instance.
  • If the hosting interface requires a single exported function, require an authenticated, unpredictable stream identifier and store state in a map keyed by both caller identity and stream identity.
  • Do not trust a caller-supplied stream identifier by itself; bind it to the authenticated caller to prevent enumeration or cross-user access.
  • Delete stream state immediately after a final flush, reset, timeout, or cancellation.
  • Apply expiration and capacity limits to any server-side state map.
  • Prevent one caller from resetting or reconfiguring another caller's stream.
  • Add concurrency tests that interleave init, process, flush, and reset operations from multiple simulated users and verify that no content or configuration crosses session boundaries.

T09 · Insecure Skill Coding Practices

Warning
Location
index.ts:10
Finding

Unbounded Stream Buffer Allows Memory-Exhaustion Denial of Service

Content
View full analysis

Vulnerability Details

File Location: index.ts, lines 10-14 and 68-109
Vulnerability Type: Uncontrolled memory allocation and unbounded input accumulation
Risk Level: Medium

Vulnerable Code

typescript
z.object({
  action: z.literal("process"),
  chunk: z.string(),
  flush: z.boolean().optional().default(false),
}),
typescript
case "process": {
  const { chunk, flush } = validatedParams;

  // Add to buffer
  buffer += chunk;

  // Duplicate removal
  if (config.remove_duplicates && buffer.includes(lastOutput) && lastOutput.length > 0) {
    buffer = buffer.replace(lastOutput, "");
  }

  let output = "";

  // Adaptive buffer size
  let adaptiveBufferSize = config.buffer_size;
  if (buffer.includes("```") || buffer.includes("$")) {
    adaptiveBufferSize = 50;
  } else if (chunk.length > 0 && SENTENCE_ENDINGS.has(chunk[chunk.length - 1])) {
    adaptiveBufferSize = 5;
  }

  // Process output if the buffer is sufficiently large or flushing is forced
  if (buffer.length >= adaptiveBufferSize || flush) {
    // Repair incomplete sentences
    if (config.fix_incomplete_sentences && !flush) {
      // Find the last complete sentence
      let lastEndIndex = -1;
      for (let i = buffer.length - 1; i >= Math.max(0, buffer.length - 20); i--) {
        if (SENTENCE_ENDINGS.has(buffer[i])) {
          lastEndIndex = i + 1;
          break;
        }
      }

      if (lastEndIndex > 0) {
        // Emit the complete sentence
        output = buffer.slice(0, lastEndIndex);
        // Retain the incomplete portion
        buffer = buffer.slice(lastEndIndex);
      } else if (flush) {
        // Emit all content during a forced flush
        output = buffer;
        buffer = "";
      }
    } else {
      // Emit the entire buffer when sentence repair is disabled
      output = buffer;
      buffer = "
...[truncated 2255 chars]
Remediation
View remediation

Remediation Suggestions

  • Add a strict maximum length to chunk in the validation schema, such as z.string().max(MAX_CHUNK_SIZE).
  • Enforce a separate maximum accumulated buffer size for every stream.
  • Reject oversized input before concatenation to avoid temporarily allocating an oversized combined string.
  • When the accumulated limit is reached, safely emit a bounded segment, return a controlled error, or truncate according to a clearly documented policy.
  • Ensure forced segmentation makes forward progress even when no sentence-ending character is present.
  • Use per-caller quotas, request-size limits, rate limiting, and runtime memory limits as defense-in-depth controls.
  • Clear abandoned buffers after a short inactivity timeout and when streams are cancelled.
  • Add tests using very large single chunks and repeated chunks without punctuation, verifying that memory use remains bounded.
  • Correct the documentation claiming memory usage below 1 KB, because actual memory use depends on attacker-controlled buffered content.
Vulnerability Patterns
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • 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
Findings (5)

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The entire skill description is written only in Chinese, including headings, usage, and parameter explanations, with no indication that users may choose another language or that the skill is intentionally limited to a Chinese-speaking context. Under the policy, a language or locale constraint should be optional or explicitly justified rather than implicitly imposed.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The skill's user-facing documentation is written entirely in Chinese, including headings and parameter explanations, with no indication that users may choose another language or that the skill is region-specific. This can violate language/locale policy requirements when a skill is presented to a broader audience without opt-in.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

This file contains natural-language descriptions and comments such as the type-definition comment and skill description in Chinese, but does not indicate that the skill is intended only for Chinese-speaking users or provide any language opt-in. That can violate language/locale policy because it implicitly forces one language for maintainers or users interacting with the skill.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The docstring describing the skill is entirely in Chinese and appears to be the primary natural-language description of the skill's purpose. Because the file does not offer an alternative language or explain a justified region-specific constraint, it may impose a language choice without user opt-in.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
95% confidence
Finding

Inside the if (config.fix_incomplete_sentences && !flush) block, the nested else if (flush) can never execute because flush is already known to be false. The inline comment says forced flush will output all content, but the actual forced-flush behavior is handled by the outer else branch at L107-L110 instead.

Content

No source excerpt is available for this finding.