Back to skill

Security audit

qiguo-strategy-game

Security checks for vulnerabilities and agentic risk

Overview

This is a playable browser game skill, but its optional online multiplayer relay is inconsistently disclosed and has real network-safety weaknesses.

Install only if you are comfortable with the optional multiplayer relay. Local AI, hotseat, and same-browser play are lower risk; avoid exposing net-server.js to a LAN or the internet, and assume anyone who can reach the relay and guess the room can interfere with the match and potentially affect the game page.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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
Findings (4)

T09 · Insecure Skill Coding Practices

Error
Location
assets/campaign-mode.html:2142
Finding
Unauthenticated Network Messages Enable DOM-Based Cross-Site Scripting<![CDATA[ ## Vulnerability Details **File Location**: `assets/campaign-mode.html:2142-2144, 2164, 2183-2192`; `assets/skirmish-mode.html:2608-2610, 2631, 2651-2664, 2814`; `net-server.js:91-106` **Vulnerability Type**: DOM-based cross-site scripting through untrusted multiplayer state **Risk Level**: High ### Vulnerable Code Campaign mode accepts arbitrary relay messages and dispatches them according to the attacker-controlled `t` property: ```js ws=new WebSocket(opts.url||wsUrlVal); ws.onopen=()=>{ try{ ws.send(JSON.stringify({t:'HELLO', room, role})); }catch(e){} flush(); }; 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 campaign snapshot contains rendered HTML: ```js function snapshot(){ return { t:'STATE', turn, weather, combo, over:battleResolved, winner:lastWinner, turnCount, units: JSON.parse(JSON.stringify(units)), terrain: JSON.parse(JSON.stringify(terrain)), scenario: JSON.parse(JSON.stringify(scenario)), logHtml: (document.getElementById('log')?document.getElementById('log').innerHTML:''), myFormation, enemyFormation, myStrats, enStrats, myStratGenName, enStratGenName, stratUsed: {me:stratUsed.me, en:stratUsed.en}, myCountry, enemyCountry }; } ``` A received `STATE` message is trusted and its `logHtml` property is assigned directly to `innerHTML`: ```js 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.myCountry; myFormation=m.enemyFormation; enemyFormation=m.myFormation; myStrats=m.enStrats; enStrats=m.myStrats; myStratGenName=m.enStratGenName; enStratGenName=m.myStratGenName; stratUsed={me:m.stratUsed.en, en:m.stratUsed.me}; PLAYER=m.enemyCountry; battleEnemyKey=m.myCountry; currentBattleO=null; const le=docu ...[truncated 2954 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Do not transmit rendered HTML. Replace `logHtml` and HTML-formatted `logLines` with structured records containing fixed event types and plain-text fields. 2. Render all remote text with `textContent`, not `innerHTML`. 3. If limited formatting is required, construct approved DOM elements programmatically and assign every dynamic value through `textContent`. 4. Validate each received message against a strict schema before dispatch: - Require an exact set of properties. - Enforce property types, lengths, and enumerated values. - Reject unknown fields and unexpected message types. 5. Authenticate each room member and authorize which role may send `STATE`. 6. Add a restrictive Content Security Policy that disallows inline script and inline event handlers. 7. Treat BroadcastChannel messages as untrusted as well; another same-origin page can send forged channel messages. 8. Add automated tests using malicious strings in every remotely supplied field to verify that no markup is interpreted. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
net-server.js:91
Finding
Missing Room Authentication and Server-Side Action Authorization<![CDATA[ ## Vulnerability Details **File Location**: `net-server.js:91-106`; `assets/campaign-mode.html:2202-2226`; `assets/skirmish-mode.html:2674-2698` **Vulnerability Type**: Unauthorized room access and multiplayer action forgery **Risk Level**: High ### Vulnerable Code The relay accepts any room name supplied by a client and adds the connection without authentication or a peer limit: ```js 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); ``` The campaign host registers sensitive message handlers without associating messages with an authenticated role: ```js function netRegisterHost(){ Net.on('JOIN', ()=>{ if(units.length) Net.send(snapshot()); }); Net.on('ACT', hostApplyAct); Net.on('END', ()=>{ if(turn==='en'){ turn='me'; startHotseatTurn('me'); } }); } ``` The host then applies client-supplied actions without validating turn ownership, unit ownership, legal coordinates, movement range, or attack range: ```js function hostApplyAct(a){ if(a.kind==='move'){ const u=unitById(a.uid); if(u){u.row=a.r;u.col=a.c;u.moved=true;} } else if(a.kind==='attack'){ const att=unitById(a.aUid), def=unitById(a.dUid); if(att&&def){ selected=att; doAttack(att,def); selected=null; } } else if(a.kind==='skill'){ const u=unitById(a.uid); if(u){ selected=u; hostCastSkill(u,a.idx); selected=null; } } else if(a.kind==='strat'){ hostCastStrat(a.key); } render(); renderBattleControls(); renderSelBox(); netAfterAction(); } ``` Skirmish mode contains the same authorization logic: ```js function netRegisterHost(){ Net.on('JOIN', ()=>{ if(units.length) Net.send(snapshot()); }); Net.on('ACT' ...[truncated 3300 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Generate a cryptographically random room identifier rather than using a predictable default. 2. Generate separate unguessable host and client credentials for every room. 3. Bind each authenticated connection to a server-maintained role; never trust a client-supplied role field by itself. 4. Limit every match room to exactly one host and one client unless spectators are explicitly implemented with read-only privileges. 5. Attach sender identity and role metadata to internally forwarded events. 6. Permit only the authenticated host to send `STATE`, and only the authenticated client to send client-side actions. 7. Validate every action on the host: - Confirm that it is the remote side's turn. - Confirm that the unit belongs to the authenticated sender. - Check map bounds, occupancy, movement allowance, legal path, and terrain rules. - Check target side, attack range, line of sight, resource costs, and whether the unit has already acted. - Reject replayed or out-of-order actions using sequence numbers. 8. Validate `END`, skill, and strategy messages under the same authorization model. 9. Rate-limit failed room joins and invalid actions. 10. Avoid sending snapshots to arbitrary `JOIN` senders until they have authenticated. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
net-server.js:20
Finding
Relay Is Exposed Beyond Loopback Without Origin or Handshake Access Controls<![CDATA[ ## Vulnerability Details **File Location**: `net-server.js:20-31, 145-147` **Vulnerability Type**: Insecure network exposure and missing WebSocket origin validation **Risk Level**: Medium ### Vulnerable Code The upgrade handler validates only the presence of a WebSocket key: ```js server.on('upgrade', (req, socket) => { const key = req.headers['sec-websocket-key']; if (!key) { socket.destroy(); return; } const accept = crypto.createHash('sha1').update(key + GUID).digest('base64'); socket.write( 'HTTP/1.1 101 Switching Protocols\r\n' + 'Upgrade: websocket\r\n' + 'Connection: Upgrade\r\n' + 'Sec-WebSocket-Accept: ' + accept + '\r\n\r\n' ); socket.setNoDelay(true); ``` The server is started without an explicit host, but the log message states that it is listening on loopback: ```js server.listen(PORT, () => { console.log('[qiguo-net] WebSocket relay listening on ws://127.0.0.1:' + PORT); }); ``` ### Technical Analysis In Node.js, calling `server.listen(PORT)` without a host generally binds to an unspecified address, commonly all available IPv6 or IPv4 interfaces depending on the platform. This is inconsistent with the displayed `127.0.0.1` address and may unintentionally expose the relay to the local network. The WebSocket upgrade handler does not validate: - The request `Origin`. - The request path. - A required WebSocket subprotocol. - Authentication credentials. - Whether the connection should be permitted from the source address. WebSocket connections are not protected by the browser's ordinary same-origin restrictions in the same manner as fetch requests. A malicious web page can attempt a WebSocket connection to a reachable relay, and the server currently accepts such upgrades if a WebSocket key is present. This exposure materially increases the reachability of the room-injection and action-forgery vulnerabilities. ### Attack Path 1. A user runs `node net-server.js` expecting the relay to be available only ...[truncated 869 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Bind to loopback by default: ```js server.listen(PORT, '127.0.0.1', () => { console.log(`[qiguo-net] WebSocket relay listening on ws://127.0.0.1:${PORT}`); }); ``` 2. Require an explicit command-line option or environment variable before binding to a LAN-accessible interface. 3. Report the actual bind address rather than a hardcoded address. 4. Validate the `Origin` header against an explicit allowlist. Account for the expected origin behavior of the intended preview environment. 5. Require a dedicated, unpredictable WebSocket path and an expected subprotocol. 6. Require room authentication independently of origin validation. 7. Use TLS (`wss://`) when traffic crosses devices or untrusted networks. 8. Document firewall requirements and clearly warn users when enabling LAN exposure. 9. Reject malformed upgrade requests and validate WebSocket version and connection headers. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
net-server.js:39
Finding
Unbounded WebSocket Frame Buffering Enables Relay Denial of Service<![CDATA[ ## Vulnerability Details **File Location**: `net-server.js:39-70` **Vulnerability Type**: Memory and CPU exhaustion through oversized or incomplete WebSocket frames **Risk Level**: Medium ### Vulnerable Code The relay accumulates all received data and repeatedly concatenates it without enforcing a maximum frame size: ```js socket.on('data', (data) => { buf.chunks.push(data); buf.len += data.length; let buffer = Buffer.concat(buf.chunks, buf.len); // Parse multiple frames that may have arrived together. 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); } }); ``` ### Technical Analysis The parser accepts client-declared 64-bit payload lengths but imposes no maximum frame or message size. If the advertised payload is incomplete, processing exits the loop while retaining the accumulated bytes in `buf`. On every new data event, the code calls `Buffer.concat` over the entire retained input. An attacker can therefore: - Advertis ...[truncated 1565 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Enforce a conservative maximum frame and message size before buffering payload data. 2. Immediately close the connection with an appropriate WebSocket close code when a declared size exceeds the limit. 3. Reject unmasked client-to-server frames as protocol violations. 4. Add handshake, frame-completion, idle, and overall connection timeouts. 5. Limit concurrent connections per source address and total connections per server. 6. Limit each room to the intended number of authenticated peers. 7. Apply byte-rate and message-rate limits. 8. Replace repeated whole-buffer concatenation with an incremental parser that consumes chunks without repeatedly copying all retained input. 9. Validate 64-bit lengths safely and reject values beyond JavaScript's safe integer range. 10. Consider using a maintained WebSocket implementation that already provides protocol validation, payload limits, fragmentation handling, and backpressure controls. 11. Monitor socket backpressure and avoid unbounded outbound writes to slow clients. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (12)

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding
The skill claims to be a local launcher with 'zero setup' and 'no server, no network', but its documented behavior includes running a WebSocket relay server for cross-device multiplayer. That mismatch is dangerous because it can mislead users and higher-level agents into enabling network-exposed functionality they did not intend to authorize or review.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
The documentation explicitly says the skill is 'purely a launcher' with 'no server, no network', while elsewhere instructing users to run a bundled WebSocket relay for remote play. Contradictory safety claims undermine informed consent and may cause users to run network services under the false assumption that the skill stays fully local.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
This is the same core issue as above: the overview gives a strong offline-only assurance, but later instructions direct users to start net-server.js and use WebSocket networking. Such contradictions are risky because they hide attack surface expansion from local file preview to active network service operation.

Context-Inappropriate Capability

Medium
Confidence
90% confidence
Finding
Documenting execution of a Node.js WebSocket relay server materially expands the skill's behavior beyond copying and previewing static HTML. Even if the server is intended for gameplay, bundling and instructing execution of network-listening code increases exposure to misuse, insecure deployment, and misunderstanding by users and invoking agents.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
The HTML implements multiplayer transports using BroadcastChannel and WebSocket, including cross-device play via a configurable ws:// server, which exceeds the stated 'self-contained local HTML' behavior. This expands the attack surface from local-only rendering to network-connected messaging, creating risks around unexpected network access, peer interaction, and misleading deployment assumptions in a preview environment.

Intent-Code Divergence

Medium
Confidence
86% confidence
Finding
The tutorial explicitly advertises cross-device online play requiring an external net-server.js, contradicting the skill's 'self-contained HTML' and 'zero setup' claims. While not code execution by itself, this is a security-relevant integrity issue because it normalizes external connectivity and hidden dependencies the user was not told about.

Description-Behavior Mismatch

Medium
Confidence
96% confidence
Finding
The skill is described as a self-contained local HTML game, but the code also enables cross-device WebSocket multiplayer. That expands the trust boundary from a local-only preview into remote network communication, creating unnecessary attack surface and a mismatch between stated capability and actual behavior.

Context-Inappropriate Capability

Medium
Confidence
95% confidence
Finding
The page contains unjustified remote networking for a game skill whose purpose is simply to open a browser game. Even though the current default points to localhost, the user-editable WebSocket URL allows arbitrary remote endpoints, enabling unsolicited outbound connections and remote interaction channels from within the previewed HTML.

Description-Behavior Mismatch

High
Confidence
98% confidence
Finding
This file implements a standalone WebSocket relay server that accepts inbound network connections, upgrades HTTP requests to WebSocket, maintains rooms, and forwards arbitrary JSON messages between clients. That materially exceeds the skill's declared purpose of copying a self-contained HTML game into the workspace and opening it locally, creating unnecessary network-exposed capability and increasing attack surface without justification in the manifest.

Context-Inappropriate Capability

High
Confidence
96% confidence
Finding
The code provides unjustified network-server functionality for a skill described as launching a zero-setup self-contained browser game. Because it opens a listener and relays attacker-controlled frames with minimal validation, the skill can be repurposed as a generic local relay service, which is dangerous in an agent/workspace context where users may not expect any background server capability.

Intent-Code Divergence

Medium
Confidence
90% confidence
Finding
The header comment explicitly states that this is an online battle WebSocket signaling/relay server, directly contradicting the manifest's claim that the skill simply opens a self-contained HTML game with zero setup. That discrepancy is a strong indicator of undeclared capability, which is risky because reviewers and users may underestimate the presence of a network service and its associated exposure.

Hidden Instructions

High
Category
Prompt Injection
Content
��*�}��;�@���T]@���p	�f�ձ��m���V��

^m�''
s*������T��D:)��!�<��N6��i,���j��`�KT��EG�hI��
�[9���7�p'��3�`���rNo�@{
	x��l�A��B L��
+W`K���ۍ�ai&��j�Ѡ2�2`��.6�
Confidence
88% confidence
Finding
The matched Unicode bidi control character indicates obfuscated text content inside the skill bundle. In a packaged skill that delivers executable HTML/JS, hidden directional controls can conceal instructions, code, filenames, or logic from reviewers and increase the risk of masked malicious behavior.

Static analysis

No suspicious patterns detected.