Back to skill

Security audit

qiguo-strategy-game

Security checks for vulnerabilities and agentic risk

Overview

This is a real browser game launcher, but its optional multiplayer relay is under-scoped and vulnerable enough that users should review it before installing.

Install only if you are comfortable with a Chinese-language browser game that stores progress locally. Avoid using cross-device multiplayer or running net-server.js on a reachable network unless you trust all participants and can contain the relay, because unauthenticated peers may tamper with the match, inject browser-rendered content, or exhaust the relay process.

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
assets/campaign-mode.html:2144
Finding

Unauthenticated relay peers can inject executable HTML into network-game clients

Content
View full analysis

Vulnerability Details

File Location: net-server.js:92-103; assets/campaign-mode.html:2144, 2179-2192; assets/skirmish-mode.html:2610, 2647-2649, 2814
Vulnerability Type: Unauthenticated message injection leading to DOM-based cross-site scripting
Risk Level: High

Technical Analysis

The WebSocket relay permits any connected peer to join an arbitrary room by sending a HELLO message. It does not authenticate the peer, validate the claimed role, limit a room to an authenticated host and client, or enforce which message types each role may send.

js
// net-server.js:92-103
if (msg.t === 'HELLO') {
  peer.room = String(msg.room || 'qiguo');
  if (!rooms[peer.room]) rooms[peer.room] = new Set();
  rooms[peer.room].add(peer);
  const n = rooms[peer.room].size;
  peer.socket.write(encodeFrame(0x1, Buffer.from(JSON.stringify({
    t: 'ROOM',
    room: peer.room,
    size: n
  }))));
  if (n >= 2) {
    broadcast(peer.room, { t: 'READY' }, null);
  }
  return;
}
if (peer.room) broadcast(peer.room, msg, peer);

Consequently, any peer in the room can send a forged STATE message, and the relay broadcasts it without verifying that it originated from the legitimate host.

The campaign client dynamically dispatches incoming messages based solely on the attacker-controlled t property:

js
// assets/campaign-mode.html:2144
ws.onmessage=e=>{
  let m;
  try { m=JSON.parse(e.data); } catch(e) { return; }
  if(m&&m.t&&h[m.t]) h[m.t](m);
};

The client registers onClientState as the handler for STATE and writes the supplied logHtml directly into the DOM:

js
// assets/campaign-mode.html:2179-2192
Net.on('STATE', onClientState);
Net.send({t:'JOIN'});

function onClientState(m){
  units = m.units.map(u=>Object.assign({},u));
  terrain = m.terrain; scenario = m.scenario;
  turn=m.turn; weather=m.weather; combo=m.combo; turnCount=m.turnCount;
  battleResolved=m.over; lastWinner=m.winner;
  myCountry=m.enemyCountry; enemyCountry=m.myCou
...[truncated 2888 chars]
Remediation
View remediation

Remediation Suggestions

  1. Generate an unguessable room secret when the host creates a room, and require clients to prove possession of it.
  2. Assign roles server-side rather than trusting the role value supplied by a client.
  3. Track the authenticated host connection for each room and accept STATE messages only from that connection.
  4. Restrict clients to an explicit allowlist of permitted action messages and validate every message against a strict schema.
  5. Enforce a maximum of one host and the intended number of clients per room.
  6. Replace remote log HTML with structured, plain-text log records and render them using textContent.
  7. If limited formatting is required, construct known elements through DOM APIs or apply a strict allowlist sanitizer that removes event handlers, scripts, dangerous URLs, and unsupported elements.
  8. Add a restrictive Content Security Policy that disallows inline scripts and inline event handlers as defense in depth.
  9. Reject malformed state objects before accessing or rendering their fields.

T09 · Insecure Skill Coding Practices

Warning
Location
net-server.js:36
Finding

Unbounded WebSocket frame buffering enables remote denial of service

Content
View full analysis

Vulnerability Details

File Location: net-server.js:36-68
Vulnerability Type: Unbounded network-buffer allocation and resource exhaustion
Risk Level: Medium

Technical Analysis

The relay accumulates all incoming bytes for a connection and repeatedly concatenates the buffered chunks. It accepts extended WebSocket frame lengths but imposes no maximum frame size or per-connection buffer limit.

js
// net-server.js:36-68
socket.on('data', (data) => {
  buf.chunks.push(data);
  buf.len += data.length;
  let buffer = Buffer.concat(buf.chunks, buf.len);

  while (buffer.length >= 2) {
    const b0 = buffer[0], b1 = buffer[1];
    const opcode = b0 & 0x0f;
    const masked = (b1 & 0x80) === 0x80;
    let len = b1 & 0x7f;
    let offset = 2;

    if (len === 126) {
      if (buffer.length < offset + 2) break;
      len = buffer.readUInt16BE(offset);
      offset += 2;
    } else if (len === 127) {
      if (buffer.length < offset + 8) break;
      const hi = buffer.readUInt32BE(offset),
            lo = buffer.readUInt32BE(offset + 4);
      len = hi * 4294967296 + lo;
      offset += 8;
    }

    let maskKey = null;
    if (masked) {
      if (buffer.length < offset + 4) break;
      maskKey = buffer.slice(offset, offset + 4);
      offset += 4;
    }

    if (buffer.length < offset + len) break;

    let payload = buffer.slice(offset, offset + len);
    if (masked && maskKey) {
      const out = Buffer.allocUnsafe(len);
      for (let i = 0; i < len; i++) {
        out[i] = payload[i] ^ maskKey[i & 3];
      }
      payload = out;
    }

    buffer = buffer.slice(offset + len);
    buf.chunks = [buffer];
    buf.len = buffer.length;
    handleFrame(peer, opcode, payload);
  }
});

When an incomplete frame is received, parsing stops while the accumulated data remains in buf. An unauthenticated client can declare a very large frame length and continue transmitting bytes without completing the frame. The server retains the data indefinitely ...[truncated 1347 chars]

Remediation
View remediation

Remediation Suggestions

  1. Define a small maximum frame size appropriate for game messages and close the connection immediately when the declared length exceeds it.
  2. Cap total buffered bytes per connection independently of the declared frame length.
  3. Apply socket read and idle timeouts so incomplete frames cannot retain memory indefinitely.
  4. Limit concurrent connections globally and per source address.
  5. Require client-to-server frames to be masked and reject malformed WebSocket frames.
  6. Validate extended lengths before arithmetic or allocation and reject unsafe or unsupported values.
  7. Avoid repeatedly concatenating the complete accumulated buffer; use a bounded incremental parser.
  8. Add backpressure handling and terminate peers that continuously send data faster than it can be processed.
  9. Consider using a maintained WebSocket implementation with built-in payload limits and protocol validation rather than a custom parser.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (23)

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

The skill is described as a simple local launcher with 'zero setup' and 'no server, no network,' yet its documented behavior includes running net-server.js, opening a WebSocket relay, listening on a port, and enabling cross-device multiplayer. This materially expands the attack surface from local file presentation to network service operation, which could expose the host or mislead users and agents about the trust boundary.

Content

No source excerpt is available for this finding.

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
- **普通模式 (Skirmish)** — `skirmish-mode.html`: pick a state + scenario, fast single battle. **10 历史名局** (长平/即墨火牛/宜阳/蓟城/桂陵/鄢郢/函谷关/邯郸/伊阙/阏与) each with bespoke terr

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
- **普通模式 (Skirmish)** — `skirmish-mode.html`: pick a state + scenario, fast single battle. **10 历史名局** (长平/即墨火牛/宜阳/蓟城/桂陵/鄢郢/函谷关/邯郸/伊阙/阏与) each with bespoke terr

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
- **普通模式 (Skirmish)** — `skirmish-mode.html`: pick a state + scenario, fast single battle. **10 历史名局** (长平/即墨火牛/宜阳/蓟城/桂陵/鄢郢/函谷关/邯郸/伊阙/阏与) each with bespoke terr

Description-Behavior Mismatch

High
Category
Not specified by scanner
Confidence
97% confidence
Finding

The file materially expands the skill’s capabilities beyond the declared behavior: instead of only launching a local single-file HTML game, it also provides a standalone WebSocket relay for multiplayer signaling. That mismatch is security-relevant because hidden network services increase attack surface, create undisclosed communication paths, and can enable remote interaction with the user’s environment without being justified by the manifest.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

High
Category
Not specified by scanner
Confidence
98% confidence
Finding

This code implements an inbound HTTP/WebSocket server that accepts arbitrary upgrade requests and relays messages between peers, which is far more sensitive than merely opening a browser game file. In the skill context, that extra server capability is unjustified and dangerous because it exposes a listening service, permits untrusted clients to connect, and lacks authentication, origin checks, rate limiting, or resource controls.

Content

No source excerpt is available for this finding.

Ae4

Medium
Category
analysis-evasion
Confidence
80% confidence
Finding

Suspicious Unicode normalization or mixed-script content

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
72% confidence
Finding

The skill instructs the agent to use shell operations and references environment-dependent paths, but it does not declare an explicit tool/permission scope. That weakens least-privilege controls and can let a launcher skill gain broader execution capability than its stated purpose requires.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The documentation explicitly says the skill is 'purely a launcher' with 'no server, no build step, no network,' but elsewhere documents bundled WebSocket relay functionality for cross-device play. Security-relevant self-contradictions like this can cause operators or agents to approve or invoke the skill under false assumptions about its behavior.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

Contradictory claims about whether the skill uses server/network functionality undermine reliable security review and safe execution. An agent may follow the 'no network' assurance while the skill still contains instructions to start a listener and relay traffic, creating a documentation-driven trust gap.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
75% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · assets/campaign-mode.html (reported line 633)May include surrounding context.

html
const milBtns=gov?`<div class="row"><button class="btn" id="btnMarch" ${enemyAdj.length?'':'disabled'}>⚔ 出征(攻邻城)</button>
      <button class="btn ghost" id="btnMove" ${ownAdj.length?'':'disabled'}>⇄ 调兵</button></div>`:'';
  const extraBtns=gov?`<div class="row"><button class="btn ghost" id="btnBuild">🏗 营造</button>${c.armory.length?`<button class="btn ghost" id="btnEquip">🎒 装备库(${c.armory.length})</button>`:''}</div>`:'';
  const capList=(gov&&captives.length)?`<div style="margin-top:8px;"><b>俘虏(可登用 / 放回)</b>${captives.map((cp,i)=>`<div class="genrow"><span><span class="nm">${cp.gen.name}</span> <span class="st">${'★'.repeat(cp.gen.star)}</span></span><span><span class="ut">${UNIT_TYPES[cp.gen.unit].name}</span></span><button class="btn ghost" style="padding:4px 8px;font-size:11px;" data-cap="${i}">登用(300金)</button><button class="btn ghost" style="padding:4px 8px;font-size:11px;" data-rel="${i}">放回换赎金</button></div>`).join('')}</div>`:'';
  const gensHtml=c.gens.map(g=>`<div class="genrow"><span><span class="nm">${g.name}</span> <span class="st">${'★'.repeat(g.star)}</span></span><span><span class="ut">${UNIT_TYPES[g.unit].name}</span> · <span class="tp">兵${g.troops}</span> · 武${g.wu}统${g.tong}智${g.zhi}</span></div>`).join('')||'<div class="muted">(无驻军)</div>';
  host.innerHTML=`<h2>${c.name} ${c.cap?'👑':''}</h2>
    <div class="kv"><b>所属</b>:${COUNTRIES[c.owner].name} <b>地形</b>:${terrainName(c.tag)}</div>

Session Persistence

Medium
Category
Rogue Agent
Confidence
75% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · assets/campaign-mode.html (reported line 638)May include surrounding context.

html
const milBtns=gov?`<div class="row"><button class="btn" id="btnMarch" ${enemyAdj.length?'':'disabled'}>⚔ 出征(攻邻城)</button>
      <button class="btn ghost" id="btnMove" ${ownAdj.length?'':'disabled'}>⇄ 调兵</button></div>`:'';
  const extraBtns=gov?`<div class="row"><button class="btn ghost" id="btnBuild">🏗 营造</button>${c.armory.length?`<button class="btn ghost" id="btnEquip">🎒 装备库(${c.armory.length})</button>`:''}</div>`:'';
  const capList=(gov&&captives.length)?`<div style="margin-top:8px;"><b>俘虏(可登用 / 放回)</b>${captives.map((cp,i)=>`<div class="genrow"><span><span class="nm">${cp.gen.name}</span> <span class="st">${'★'.repeat(cp.gen.star)}</span></span><span><span class="ut">${UNIT_TYPES[cp.gen.unit].name}</span></span><button class="btn ghost" style="padding:4px 8px;font-size:11px;" data-cap="${i}">登用(300金)</button><button class="btn ghost" style="padding:4px 8px;font-size:11px;" data-rel="${i}">放回换赎金</button></div>`).join('')}</div>`:'';
  const gensHtml=c.gens.map(g=>`<div class="genrow"><span><span class="nm">${g.name}</span> <span class="st">${'★'.repeat(g.star)}</span></span><span><span class="ut">${UNIT_TYPES[g.unit].name}</span> · <span class="tp">兵${g.troops}</span> · 武${g.wu}统${g.tong}智${g.zhi}</span></div>`).join('')||'<div class="muted">(无驻军)</div>';
  host.innerHTML=`<h2>${c.name} ${c.cap?'👑':''}</h2>
    <div class="kv"><b>所属</b>:${COUNTRIES[c.owner].name} <b>地形</b>:${terrainName(c.tag)}</div>

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The HTML includes full multiplayer networking logic, including BroadcastChannel and WebSocket transports, which materially expands the skill's capabilities beyond a self-contained local preview game. In a skill advertised as zero-setup local content, hidden networking increases attack surface by enabling browser-to-browser or browser-to-server communication, creating unexpected data-flow and trust-boundary risks.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The code can open arbitrary WebSocket connections (defaulting to ws://127.0.0.1:8770 and allowing user-supplied URLs), which is not necessary to simply launch a local browser game. This can be abused to initiate unexpected network activity from the preview environment, potentially interacting with local services or external servers without that capability being justified by the stated skill purpose.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The UI advertises a server-backed multiplayer option with a configurable WebSocket URL but does not provide a prominent warning that gameplay/session state may be transmitted to a server. In a skill expected to be local-only, this omission undermines informed consent and could lead users to expose session metadata to untrusted endpoints.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The HTML includes optional cross-device multiplayer behavior even though the skill is described as a local 'copy and open' browser game. Adding network capability expands the trust boundary, creates outbound communication paths, and can surprise users or hosts that expect a fully self-contained offline artifact.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The page exposes configurable WebSocket connectivity for a game whose stated purpose is just launching a local HTML file. A user-editable ws:// endpoint lets the page connect to arbitrary servers, enabling unnecessary data exchange and increasing the attack surface beyond what the skill promises.

Content

No source excerpt is available for this finding.

Vague Triggers

Low
Category
Not specified by scanner
Confidence
91% confidence
Finding

The "When To Use" section allows activation on phrases like "打开战棋游戏", "一起玩", and especially the catch-all "or similar," which are broad enough to overlap with ordinary conversation about games rather than this specific skill. The file gives positive examples but no explicit exclusion conditions or negative examples to bound when the skill should not activate.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
97% confidence
Finding

The document declares lang="zh-CN", and the visible UI text throughout the page is entirely in Simplified Chinese. For an all-file-types language policy check, this is a locale constraint presented without any opt-in, fallback, or explanation that the skill is intentionally region-specific.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Low
Category
Not specified by scanner
Confidence
83% confidence
Finding

The file stores achievements, equipment counts, codex progress, and login streak data in localStorage, including daily-return tracking. Persistent user progress may be reasonable for a game itself, but it is not justified by the manifest's narrow claim that the skill just copies and opens the HTML for immediate play.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
98% confidence
Finding

The document is explicitly marked as zh-CN and all visible UI text is Chinese, with no indication that users can choose another language or that the skill is intentionally limited to a China-specific audience. This can violate language/locale policy when a skill forces a specific locale without user opt-in.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
88% confidence
Finding

The tutorial bullet labeled '联机对战' says '同设备双人轮流' or cross-device play, but same-device turn-taking is implemented separately as hotseat mode while '联机对战' has its own BroadcastChannel/WebSocket paths. This documentation conflates two different modes and misdescribes what the networking feature actually does.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
86% confidence
Finding

The manifest uses Chinese-only display and description text, which indicates a fixed language presentation without any visible user choice or opt-in. Under the stated policy, forcing a specific language or locale without offering a choice can be a natural-language policy violation.

Content

No source excerpt is available for this finding.