T09 · Insecure Skill Coding Practices
- Location
src/index.ts:481- Finding
Unescaped User-Controlled Skill Name Enables Markdown Content Injection
- Content
View full analysis
Vulnerability Details
File Location:
src/index.ts:481-489,src/index.ts:250-252, andsrc/index.ts:424-437
Vulnerability Type: Markdown content injection caused by insufficient input validation and output encoding
Risk Level: LowVulnerable Code
typescript const patterns = [ /^(?:对比 | 评分 | 分析 | 评测)\s*(.+?)(?:\s*--.*)?$/i, /^(?:skill 评分|skill 对比 |skill 分析)\s*(.+?)$/i, /^(?:帮我 | 请)\s*(?:对比 | 评分 | 分析 | 评测)\s*(.+?)$/i, ]; let skillName = ''; for (const pattern of patterns) { const match = content.match(pattern); if (match) { skillName = match[1].trim(); break; } }The captured value is stored without validation:
typescript const targetRating: SkillRating = { skillId: targetSkill, name: targetSkill, platform: 'clawhub',It is then interpolated directly into Markdown:
typescript let md = `## 📊 Skill 评分对比报告\n\n`; md += `**目标 Skill:** ${targetSkill.name}\n`; md += `**对比对象:** ${competitors.length} 个同类 Skill\n\n`; md += `### 综合评分\n\n`; md += `| 排名 | Skill | 平台 | 综合得分 | 功能 | 代码 | 文档 | 评价 | 更新 | 安装 |\n`; md += `|------|-------|------|---------|------|------|------|------|------|------|\n`; allSkills.forEach(skill => { const rankEmoji = skill.rank === 1 ? '🥇' : skill.rank === 2 ? '🥈' : skill.rank === 3 ? '🥉' : ''; md += `| ${rankEmoji} ${skill.rank} | ${skill.name} | ${skill.platform} | **${skill.totalScore}** | `; md += `${skill.scores.functionality} | ${skill.scores.codeQuality} | ${skill.scores.documentation} | `; md += `${skill.scores.userReviews} | ${skill.scores.updateFrequency} | ${skill.scores.installation} |\n`; });Technical Analysis
The command parser permits arbitrary characters in the captured Skill name through
(.+?). The resulting value is copied intotargetRating.nameand inserted into both ordinary Markdown and a Markdown table without contextual encoding.An attacker can supply Markdown metacharacters such as pipes, line breaks, headings, links, or image ...[truncated 1482 chars]
- Remediation
View remediation
Remediation Suggestions
- Validate the requested Skill identifier before using it. If Skill names follow a registry format, enforce a strict allowlist and length limit:
typescript const SKILL_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/; if (!SKILL_NAME_PATTERN.test(skillName)) { return { type: 'text', content: 'Invalid Skill identifier.', }; }-
Apply context-specific Markdown escaping even after validation. Escape at least backslashes, pipes, brackets, parentheses, backticks, asterisks, underscores, angle brackets, and line breaks as appropriate for the output context.
-
Use a separate table-cell encoder because pipes and newline characters can alter Markdown table structure:
typescript function escapeMarkdownTableCell(value: string): string { return value .replace(/\\/g, '\\\\') .replace(/\|/g, '\\|') .replace(/\r?\n/g, ' ') .replace(/([`*_[\]<>])/g, '\\$1'); }-
Prefer structured UI components or plain-text responses when displaying untrusted values if the SDK supports them.
-
Add security tests covering malicious names containing table delimiters, line breaks, links, image syntax, headings, HTML-like text, backticks, and excessively long input.
