From b7c6c5df7bd9bb8f9b6218d644125985c8d611f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mih=C3=A1ly=20Andr=C3=A1s=20T=C3=B3th?= Date: Fri, 17 Jul 2026 13:35:55 +0200 Subject: [PATCH] fix SVG Injection problem --- src/api/badge.js | 30 +++++++++++++++++++----------- src/api/tag.js | 28 +++++++++++++++++----------- src/lib/sanitize.js | 24 ++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 22 deletions(-) create mode 100644 src/lib/sanitize.js diff --git a/src/api/badge.js b/src/api/badge.js index 8dccff0..cbe890b 100644 --- a/src/api/badge.js +++ b/src/api/badge.js @@ -1,5 +1,6 @@ const router = require('express').Router(); const axios = require('axios'); +const { escapeSvg, safeColor } = require('../lib/sanitize'); router.get('/badge', async (req, res) => { const { @@ -16,7 +17,8 @@ router.get('/badge', async (req, res) => { try { // download icon SVG from SimpleIcons CDN - const iconUrl = `https://cdn.simpleicons.org/${icon}?viewbox=auto&size=${size}`; + const safeIcon = escapeSvg(icon); + const iconUrl = `https://cdn.simpleicons.org/${safeIcon}?viewbox=auto&size=${size}`; const iconResponse = await axios.get(iconUrl, { responseType: 'text' }); if (iconResponse.status !== 200) { return res.status(404).send('Icon not found'); @@ -33,6 +35,11 @@ router.get('/badge', async (req, res) => { } // Badge size + const safeLabel = escapeSvg(effectiveLabel); + const safeBgIcon = safeColor(bgicon, 'none'); + const safeBgLabel = safeColor(bglabel, 'none'); + const safeFontWeight = escapeSvg(fontweight); + const padding = 10; const iconSize = parseInt(size); const fontSize = Math.round(iconSize * 0.6); @@ -63,6 +70,7 @@ router.get('/badge', async (req, res) => { effectiveColor = "#222"; // fallback } } + effectiveColor = safeColor(effectiveColor, '#222'); // doboz magasság 5 pixellel nagyobb legyen az ikon méreténél const iconSizeAndPadding = iconSize + 5; @@ -71,14 +79,14 @@ router.get('/badge', async (req, res) => { let width, height, iconGroup, textElem; const fontFamily = "Verdana,Geneva,DejaVu Sans,sans-serif"; - const fontWeight = fontweight; // paraméterből + const fontWeight = safeFontWeight; // paraméterből if (labelpos === 'left') { // Külön dobozok: padding csak a széleken kell, a dobozokon belül nem! - textElem = ` - ${effectiveLabel}`; + textElem = ` + ${safeLabel}`; iconGroup = ` - + ${iconSVG} `; } else if (labelpos === 'above' || labelpos === 'below') { @@ -94,9 +102,9 @@ router.get('/badge', async (req, res) => { height = iconSizeAndPadding + textPadding + labelRectHeight; // Label háttér - const labelRect = ``; + const labelRect = ``; // Ikon háttér - const iconRect = ``; + const iconRect = ``; // Szöveg const textY = labelpos === 'above' @@ -104,7 +112,7 @@ router.get('/badge', async (req, res) => { : labelRectY + 5 + labelRectHeight / 2; textElem = ` ${labelRect} - ${effectiveLabel} + ${safeLabel} `; // Ikon @@ -115,11 +123,11 @@ router.get('/badge', async (req, res) => { } else { // Alapértelmezett: ikon balra, szöveg jobbra (függőleges közép) iconGroup = ` - + ${iconSVG} `; - textElem = ` - ${effectiveLabel}`; + textElem = ` + ${safeLabel}`; } // SVG badge string összefűzéssel, szöveg árnyékkal diff --git a/src/api/tag.js b/src/api/tag.js index 1de57bf..83c0e45 100644 --- a/src/api/tag.js +++ b/src/api/tag.js @@ -1,4 +1,5 @@ const router = require('express').Router(); +const { escapeSvg, safeColor } = require('../lib/sanitize'); router.get('/tag', (req, res) => { @@ -15,7 +16,12 @@ router.get('/tag', (req, res) => { } = req.query; const fontFamily = "Verdana,Geneva,DejaVu Sans,sans-serif"; - const fontWeight = fontweight; + const fontWeight = escapeSvg(fontweight); + const safeTag = escapeSvg(tag); + const safeLabel = escapeSvg(label); + const safeColorVal = safeColor(color, '#000000'); + const safeBgTag = safeColor(bgtag, 'none'); + const safeBgLabel = safeColor(bglabel, 'none'); // A flat badge esetén a szöveg középre igazítása miatt a szöveg pozícióját módosítjuk //const labelX = labelpos === 'left' ? 165 : 455; @@ -33,12 +39,12 @@ router.get('/tag', (req, res) => { const rect = ` - - + + - ${tag.toLowerCase()} - ${label.toUpperCase()} + ${safeTag.toLowerCase()} + ${safeLabel.toUpperCase()} `.trim(); @@ -56,15 +62,15 @@ router.get('/tag', (req, res) => { - - ${tag} - - ${label} + + ${safeTag} + + ${safeLabel} `.trim(); const svg = ` - - ${tag}: ${label} + + ${safeTag}: ${safeLabel} ${style === 'rect' ? rect : flat} `.trim(); diff --git a/src/lib/sanitize.js b/src/lib/sanitize.js new file mode 100644 index 0000000..f91a113 --- /dev/null +++ b/src/lib/sanitize.js @@ -0,0 +1,24 @@ +'use strict'; + +const COLOR_RE = /^(#[0-9a-fA-F]{3,8}|[a-zA-Z]+)$/; + +/** + * Escape special XML/HTML characters to prevent SVG injection / XSS. + * < → < > → > & → & " → " ' → ' + */ +const escapeSvg = (value) => + String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + +/** + * Return a safe CSS colour string or the fallback. + * Only simple colour names and hex values (#rgb, #rrggbb, #rrggbbaa) pass. + */ +const safeColor = (value, fallback = '#000000') => + COLOR_RE.test(value) ? value : fallback; + +module.exports = { escapeSvg, safeColor };