T09 · Insecure Skill Coding Practices
Warning
- Location
- src/index.js:49
- Finding
- Terminal Escape-Sequence Injection Through Untrusted Hacker News Content<![CDATA[ ## Vulnerability Details **File Location**: `src/index.js:49-71` and `src/index.js:161-163` **Vulnerability Type**: Unsanitized terminal output **Risk Level**: Medium ### Vulnerable Code ```js // Format story for output function formatStory(story, index) { const points = story.score || 0; const comments = story.descendants || 0; const by = story.by || 'unknown'; const time = new Date(story.time * 1000).toLocaleString('pt-BR'); let url = ''; if (story.url) { try { const urlObj = new URL(story.url); url = ` (${urlObj.hostname})`; } catch {} } return `${index + 1}. ${story.title}${url} 🔼 ${points} pontos | 💬 ${comments} comentários | por @${by} 🕐 ${time} 🔗 https://news.ycombinator.com/item?id=${story.id}`; } // Format user profile function formatUser(user) { if (!user) return 'Usuário não encontrado.'; const created = new Date(user.created * 1000).toLocaleString('pt-BR'); const about = user.about || 'Sem descrição.'; return `👤 @${user.id} 📊 Karma: ${user.karma} 📅 Criado em: ${created} 📝 Sobre: ${about}`; } ``` The item-detail path similarly prints remote text: ```js console.log(formatStory(story, 0)); if (story.text) { console.log(`\n📝 Texto: ${story.text.replace(/<[^>]*>/g, '')}`); } ``` ### Technical Analysis The application retrieves public, user-generated Hacker News data and writes fields such as `story.title`, `story.by`, `story.text`, `user.id`, and `user.about` directly to the terminal. The regular expression applied to `story.text` removes strings resembling HTML tags, but it does not remove terminal control characters or ANSI escape sequences. HTML stripping and terminal-output sanitization address different encoding contexts. If attacker-controlled content contains supported control sequences, a compatible terminal may interpret those bytes instead of displaying them literally. Relevant sequence classes include: - ANSI/CSI sequences that change colors, cursor ...[truncated 1898 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions Sanitize every remotely sourced string before writing it to a terminal. The remediation should cover all Hacker News fields rather than only `story.text`. 1. Add a centralized terminal-output sanitizer that removes: - ANSI CSI escape sequences. - OSC sequences, including hyperlink and clipboard-related sequences. - Other C0 and C1 control characters, except explicitly permitted formatting characters such as newline where required. 2. Apply the sanitizer to `story.title`, `story.by`, `story.text`, `user.id`, and `user.about`. 3. Use a maintained package designed to strip ANSI sequences, pinned through the lockfile, while separately removing non-ANSI control characters. 4. Normalize or escape line breaks where remote values are expected to occupy only one output line. 5. Add tests containing CSI, OSC, carriage-return, backspace, null-byte, and multiline payloads. 6. Consider escaping remote content into a visibly quoted representation when preserving the exact value is more important than presentation. Example hardening pattern: ```js const stripAnsi = require('strip-ansi'); function sanitizeTerminal(value, { allowNewlines = false } = {}) { let result = stripAnsi(String(value ?? '')); // Remove OSC sequences not covered by basic ANSI handling. result = result.replace(/\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)/g, ''); // Remove C0/C1 controls, optionally retaining CR/LF formatting. result = allowNewlines ? result.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, '') : result.replace(/[\x00-\x1F\x7F-\x9F]/g, ''); return result; } ``` Use the function before interpolation: ```js const title = sanitizeTerminal(story.title); const author = sanitizeTerminal(story.by); const about = sanitizeTerminal(user.about, { allowNewlines: true }); const text = sanitizeTerminal( story.text.replace(/<[^>]*>/g, ''), { allowNewlines: true } ); ``` The sanitizer should be validated against the terminal environments sup ...[truncated 90 chars]
