fix SVG Injection problem
This commit is contained in:
+19
-11
@@ -1,5 +1,6 @@
|
|||||||
const router = require('express').Router();
|
const router = require('express').Router();
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
const { escapeSvg, safeColor } = require('../lib/sanitize');
|
||||||
|
|
||||||
router.get('/badge', async (req, res) => {
|
router.get('/badge', async (req, res) => {
|
||||||
const {
|
const {
|
||||||
@@ -16,7 +17,8 @@ router.get('/badge', async (req, res) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// download icon SVG from SimpleIcons CDN
|
// 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' });
|
const iconResponse = await axios.get(iconUrl, { responseType: 'text' });
|
||||||
if (iconResponse.status !== 200) {
|
if (iconResponse.status !== 200) {
|
||||||
return res.status(404).send('Icon not found');
|
return res.status(404).send('Icon not found');
|
||||||
@@ -33,6 +35,11 @@ router.get('/badge', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Badge size
|
// Badge size
|
||||||
|
const safeLabel = escapeSvg(effectiveLabel);
|
||||||
|
const safeBgIcon = safeColor(bgicon, 'none');
|
||||||
|
const safeBgLabel = safeColor(bglabel, 'none');
|
||||||
|
const safeFontWeight = escapeSvg(fontweight);
|
||||||
|
|
||||||
const padding = 10;
|
const padding = 10;
|
||||||
const iconSize = parseInt(size);
|
const iconSize = parseInt(size);
|
||||||
const fontSize = Math.round(iconSize * 0.6);
|
const fontSize = Math.round(iconSize * 0.6);
|
||||||
@@ -63,6 +70,7 @@ router.get('/badge', async (req, res) => {
|
|||||||
effectiveColor = "#222"; // fallback
|
effectiveColor = "#222"; // fallback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
effectiveColor = safeColor(effectiveColor, '#222');
|
||||||
|
|
||||||
// doboz magasság 5 pixellel nagyobb legyen az ikon méreténél
|
// doboz magasság 5 pixellel nagyobb legyen az ikon méreténél
|
||||||
const iconSizeAndPadding = iconSize + 5;
|
const iconSizeAndPadding = iconSize + 5;
|
||||||
@@ -71,14 +79,14 @@ router.get('/badge', async (req, res) => {
|
|||||||
let width, height, iconGroup, textElem;
|
let width, height, iconGroup, textElem;
|
||||||
|
|
||||||
const fontFamily = "Verdana,Geneva,DejaVu Sans,sans-serif";
|
const fontFamily = "Verdana,Geneva,DejaVu Sans,sans-serif";
|
||||||
const fontWeight = fontweight; // paraméterből
|
const fontWeight = safeFontWeight; // paraméterből
|
||||||
|
|
||||||
if (labelpos === 'left') {
|
if (labelpos === 'left') {
|
||||||
// Külön dobozok: padding csak a széleken kell, a dobozokon belül nem!
|
// Külön dobozok: padding csak a széleken kell, a dobozokon belül nem!
|
||||||
textElem = `<rect x="0" y="0" width="${textWidth}" height="${iconBoxHeight}" rx="${radius}" fill="${bglabel}"/>
|
textElem = `<rect x="0" y="0" width="${textWidth}" height="${iconBoxHeight}" rx="${radius}" fill="${safeBgLabel}"/>
|
||||||
<text x="${textWidth / 2}" y="${iconBoxHeight / 2}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="${fontWeight}" fill="${effectiveColor}" text-anchor="middle" dominant-baseline="middle">${effectiveLabel}</text>`;
|
<text x="${textWidth / 2}" y="${iconBoxHeight / 2}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="${fontWeight}" fill="${effectiveColor}" text-anchor="middle" dominant-baseline="middle">${safeLabel}</text>`;
|
||||||
iconGroup = `
|
iconGroup = `
|
||||||
<rect x="${textWidth}" y="0" width="${iconBoxWidth + 5}" height="${iconBoxHeight}" rx="${radius}" fill="${bgicon}"/>
|
<rect x="${textWidth}" y="0" width="${iconBoxWidth + 5}" height="${iconBoxHeight}" rx="${radius}" fill="${safeBgIcon}"/>
|
||||||
<g transform="translate(${textWidth + (5 / 2)}, ${(iconBoxHeight - iconSize) / 2})">${iconSVG}</g>
|
<g transform="translate(${textWidth + (5 / 2)}, ${(iconBoxHeight - iconSize) / 2})">${iconSVG}</g>
|
||||||
`;
|
`;
|
||||||
} else if (labelpos === 'above' || labelpos === 'below') {
|
} else if (labelpos === 'above' || labelpos === 'below') {
|
||||||
@@ -94,9 +102,9 @@ router.get('/badge', async (req, res) => {
|
|||||||
height = iconSizeAndPadding + textPadding + labelRectHeight;
|
height = iconSizeAndPadding + textPadding + labelRectHeight;
|
||||||
|
|
||||||
// Label háttér
|
// Label háttér
|
||||||
const labelRect = `<rect x="0" y="${labelRectY}" width="${width}" height="${labelRectHeight}" rx="${radius}" fill="${bglabel}"/>`;
|
const labelRect = `<rect x="0" y="${labelRectY}" width="${width}" height="${labelRectHeight}" rx="${radius}" fill="${safeBgLabel}"/>`;
|
||||||
// Ikon háttér
|
// Ikon háttér
|
||||||
const iconRect = `<rect x="0" y="${iconRectY}" width="${width}" height="${iconSize + 5}" rx="${radius}" fill="${bgicon}"/>`;
|
const iconRect = `<rect x="0" y="${iconRectY}" width="${width}" height="${iconSize + 5}" rx="${radius}" fill="${safeBgIcon}"/>`;
|
||||||
|
|
||||||
// Szöveg
|
// Szöveg
|
||||||
const textY = labelpos === 'above'
|
const textY = labelpos === 'above'
|
||||||
@@ -104,7 +112,7 @@ router.get('/badge', async (req, res) => {
|
|||||||
: labelRectY + 5 + labelRectHeight / 2;
|
: labelRectY + 5 + labelRectHeight / 2;
|
||||||
textElem = `
|
textElem = `
|
||||||
${labelRect}
|
${labelRect}
|
||||||
<text x="${width / 2}" y="${textY}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="${fontWeight}" fill="${effectiveColor}" text-anchor="middle" dominant-baseline="middle">${effectiveLabel}</text>
|
<text x="${width / 2}" y="${textY}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="${fontWeight}" fill="${effectiveColor}" text-anchor="middle" dominant-baseline="middle">${safeLabel}</text>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Ikon
|
// Ikon
|
||||||
@@ -115,11 +123,11 @@ router.get('/badge', async (req, res) => {
|
|||||||
} else {
|
} else {
|
||||||
// Alapértelmezett: ikon balra, szöveg jobbra (függőleges közép)
|
// Alapértelmezett: ikon balra, szöveg jobbra (függőleges közép)
|
||||||
iconGroup = `
|
iconGroup = `
|
||||||
<rect x="0" y="0" width="${iconBoxWidth}" height="${iconBoxHeight}" rx="${radius}" fill="${bgicon}"/>
|
<rect x="0" y="0" width="${iconBoxWidth}" height="${iconBoxHeight}" rx="${radius}" fill="${safeBgIcon}"/>
|
||||||
<g transform="translate(${(iconBoxWidth - iconViewBoxWidth) / 2}, ${(iconBoxHeight - iconViewBoxHeight) / 2})">${iconSVG}</g>
|
<g transform="translate(${(iconBoxWidth - iconViewBoxWidth) / 2}, ${(iconBoxHeight - iconViewBoxHeight) / 2})">${iconSVG}</g>
|
||||||
`;
|
`;
|
||||||
textElem = `<rect x="${iconBoxWidth}" y="0" width="${textWidth}" height="${iconBoxHeight}" rx="${radius}" fill="${bglabel}"/>
|
textElem = `<rect x="${iconBoxWidth}" y="0" width="${textWidth}" height="${iconBoxHeight}" rx="${radius}" fill="${safeBgLabel}"/>
|
||||||
<text x="${iconBoxWidth + textWidth / 2}" y="${iconBoxHeight / 2}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="${fontWeight}" fill="${effectiveColor}" text-anchor="middle" dominant-baseline="middle">${effectiveLabel}</text>`;
|
<text x="${iconBoxWidth + textWidth / 2}" y="${iconBoxHeight / 2}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="${fontWeight}" fill="${effectiveColor}" text-anchor="middle" dominant-baseline="middle">${safeLabel}</text>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SVG badge string összefűzéssel, szöveg árnyékkal
|
// SVG badge string összefűzéssel, szöveg árnyékkal
|
||||||
|
|||||||
+17
-11
@@ -1,4 +1,5 @@
|
|||||||
const router = require('express').Router();
|
const router = require('express').Router();
|
||||||
|
const { escapeSvg, safeColor } = require('../lib/sanitize');
|
||||||
|
|
||||||
|
|
||||||
router.get('/tag', (req, res) => {
|
router.get('/tag', (req, res) => {
|
||||||
@@ -15,7 +16,12 @@ router.get('/tag', (req, res) => {
|
|||||||
} = req.query;
|
} = req.query;
|
||||||
|
|
||||||
const fontFamily = "Verdana,Geneva,DejaVu Sans,sans-serif";
|
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
|
// 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;
|
//const labelX = labelpos === 'left' ? 165 : 455;
|
||||||
@@ -33,12 +39,12 @@ router.get('/tag', (req, res) => {
|
|||||||
|
|
||||||
const rect = `
|
const rect = `
|
||||||
<g shape-rendering="crispEdges">
|
<g shape-rendering="crispEdges">
|
||||||
<rect width="${tagTextLength+textPadding}" height="${badgeSize}" fill="${bgtag}"/>
|
<rect width="${tagTextLength+textPadding}" height="${badgeSize}" fill="${safeBgTag}"/>
|
||||||
<rect x="${tagTextLength}" width="${labelTextLength+(textPadding*2)}" height="${badgeSize}" fill="${bglabel}"/>
|
<rect x="${tagTextLength}" width="${labelTextLength+(textPadding*2)}" height="${badgeSize}" fill="${safeBgLabel}"/>
|
||||||
</g>
|
</g>
|
||||||
<g fill="#fff" text-anchor="start" font-family="${fontFamily}" text-rendering="geometricPrecision" font-size="${badgeFontSize}">
|
<g fill="#fff" text-anchor="start" font-family="${fontFamily}" text-rendering="geometricPrecision" font-size="${badgeFontSize}">
|
||||||
<text x="${tagTextX}" y="${badgeFontSize + textPadding}" fill="${color}">${tag.toLowerCase()}</text>
|
<text x="${tagTextX}" y="${badgeFontSize + textPadding}" fill="${safeColorVal}">${safeTag.toLowerCase()}</text>
|
||||||
<text x="${labelTextX}" y="${badgeFontSize + textPadding}" fill="${color}" font-weight="${fontWeight}">${label.toUpperCase()}</text>
|
<text x="${labelTextX}" y="${badgeFontSize + textPadding}" fill="${safeColorVal}" font-weight="${fontWeight}">${safeLabel.toUpperCase()}</text>
|
||||||
</g>`.trim();
|
</g>`.trim();
|
||||||
|
|
||||||
|
|
||||||
@@ -56,15 +62,15 @@ router.get('/tag', (req, res) => {
|
|||||||
<rect width="62" height="20" fill="url(#s)"/>
|
<rect width="62" height="20" fill="url(#s)"/>
|
||||||
</g>
|
</g>
|
||||||
<g fill="#fff" text-anchor="middle" font-family="${fontFamily}" text-rendering="geometricPrecision" font-size="110" font-weight="${fontWeight}">
|
<g fill="#fff" text-anchor="middle" font-family="${fontFamily}" text-rendering="geometricPrecision" font-size="110" font-weight="${fontWeight}">
|
||||||
<text aria-hidden="true" x="165" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">${tag}</text>
|
<text aria-hidden="true" x="165" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">${safeTag}</text>
|
||||||
<text x="165" y="140" transform="scale(.1)" fill="#fff" textLength="210">${tag}</text>
|
<text x="165" y="140" transform="scale(.1)" fill="#fff" textLength="210">${safeTag}</text>
|
||||||
<text aria-hidden="true" x="455" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">${label}</text>
|
<text aria-hidden="true" x="455" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">${safeLabel}</text>
|
||||||
<text x="455" y="140" transform="scale(.1)" fill="#fff" textLength="210">${label}</text>
|
<text x="455" y="140" transform="scale(.1)" fill="#fff" textLength="210">${safeLabel}</text>
|
||||||
</g>`.trim();
|
</g>`.trim();
|
||||||
|
|
||||||
const svg = `
|
const svg = `
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="${badgeWidth}" height="${badgeSize}" role="img" aria-label="${tag}: ${label}">
|
<svg xmlns="http://www.w3.org/2000/svg" width="${badgeWidth}" height="${badgeSize}" role="img" aria-label="${safeTag}: ${safeLabel}">
|
||||||
<title>${tag}: ${label}</title>
|
<title>${safeTag}: ${safeLabel}</title>
|
||||||
${style === 'rect' ? rect : flat}
|
${style === 'rect' ? rect : flat}
|
||||||
</svg>`.trim();
|
</svg>`.trim();
|
||||||
|
|
||||||
|
|||||||
@@ -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, '"')
|
||||||
|
.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 };
|
||||||
Reference in New Issue
Block a user