From d37e78e8aed936a85355c5874a2cc7a57219ce48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20St=C3=B6lzle?= Date: Mon, 31 Aug 2026 20:44:05 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=20Fix=20polynomial=20ReDoS=20in=20?= =?UTF-8?q?validation=20regexes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace the frontmatter key regex with an index split - Stop link and XML tag patterns from rescanning on each offset - Compare trailing whitespace with endsWith instead of a regex - Accept CRLF frontmatter, which the opening regex rejected before - Coerce non-string descriptions instead of throwing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/core/frontmatter.js | 12 +++++++----- src/validate/index.js | 6 +++--- src/validate/micro-templates.js | 11 ++++++----- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/core/frontmatter.js b/src/core/frontmatter.js index 9296619..f64b5f4 100644 --- a/src/core/frontmatter.js +++ b/src/core/frontmatter.js @@ -3,7 +3,7 @@ */ export function extractFrontmatter(content) { - const match = content.match(/^---\n([\s\S]*?)\n---/m) + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/m) if (!match) { return null } @@ -14,13 +14,15 @@ export function extractFrontmatter(content) { let currentValue = '' for (const line of lines) { - const keyMatch = line.match(/^(\w+):\s*(.*)$/) - if (keyMatch && !line.startsWith(' ')) { + const separatorIndex = line.indexOf(':') + const key = separatorIndex > 0 ? line.slice(0, separatorIndex) : null + + if (key !== null && /^\w+$/.test(key) && !line.startsWith(' ')) { if (currentKey) { frontmatter[currentKey] = currentValue.trim().replace(/^["']|["']$/g, '') } - currentKey = keyMatch[1] - currentValue = keyMatch[2] + currentKey = key + currentValue = line.slice(separatorIndex + 1) } else if (currentKey) { currentValue += ' ' + line.trim() } diff --git a/src/validate/index.js b/src/validate/index.js index e273cb4..9193d6f 100644 --- a/src/validate/index.js +++ b/src/validate/index.js @@ -66,7 +66,7 @@ export async function validateSkill(skillDir) { } // Check SKILL.md length (words, excluding frontmatter) - const frontmatterMatch = content.match(/^---\n[\s\S]*?\n---/m) + const frontmatterMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---/m) const skillContent = frontmatterMatch ? content.slice(frontmatterMatch[0].length) : content const wordCount = skillContent .trim() @@ -504,7 +504,7 @@ async function checkHeadingHierarchy(skillDir, warnings) { if (inCodeBlock) continue - const headingMatch = lines[i].match(/^(#{1,6})\s+\S/) + const headingMatch = lines[i].match(/^(#{1,6})[ \t]+\S/) if (headingMatch) { const currentLevel = headingMatch[1].length if (previousLevel > 0 && currentLevel > previousLevel + 1) { @@ -540,7 +540,7 @@ async function checkDuplicateHeadings(skillDir, warnings) { const lines = data.text.split('\n') for (const line of lines) { - const headingMatch = line.match(/^#{1,6}\s+(.+)$/) + const headingMatch = line.match(/^#{1,6}[ \t]+(.+)$/) if (headingMatch) { const headingText = headingMatch[1].trim() if (headings.includes(headingText)) { diff --git a/src/validate/micro-templates.js b/src/validate/micro-templates.js index 7c63a4c..cf90035 100644 --- a/src/validate/micro-templates.js +++ b/src/validate/micro-templates.js @@ -3,7 +3,7 @@ */ function normalizeText(text) { - return (text || '').replace(/[\t\r\n]+/g, ' ').trim() + return (typeof text === 'string' ? text : '').replace(/[\t\r\n]+/g, ' ').trim() } function hasTriggerContext(text) { @@ -39,10 +39,11 @@ function checkIOTokens(text) { } function hasNoLinks(text) { - return !/(https?:\/\/|\[[^\]]+\]\([^\)]+\))/i.test(text) + return !/(https?:\/\/|\[[^\][]+\]\([^()]+\))/i.test(text) } export function validateMicroTemplate(description) { + const raw = typeof description === 'string' ? description : '' const desc = normalizeText(description) const issues = [] @@ -92,7 +93,7 @@ export function validateMicroTemplate(description) { }) } - if (/<[a-zA-Z][^>]*>/.test(desc)) { + if (/<[a-zA-Z][^<>]*>/.test(desc)) { issues.push({ severity: 'error', code: 'desc.xml', @@ -110,7 +111,7 @@ export function validateMicroTemplate(description) { } // NBSP - if (/\u00A0/.test(description)) { + if (/\u00A0/.test(raw)) { issues.push({ severity: 'warn', code: 'desc.nbsp', @@ -128,7 +129,7 @@ export function validateMicroTemplate(description) { } // Trailing whitespace - if (/[ \t]+$/.test(description)) { + if (raw.endsWith(' ') || raw.endsWith('\t')) { issues.push({ severity: 'warn', code: 'desc.trailing',