99 lines
2.4 KiB
JavaScript
99 lines
2.4 KiB
JavaScript
const path = require('path');
|
|
|
|
const DANGEROUS_EXTENSIONS = [
|
|
'.php', '.php3', '.php4', '.php5', '.phtml', '.phar',
|
|
'.jsp', '.jspx', '.jsw', '.jsv', '.jspf',
|
|
'.asp', '.aspx', '.asa', '.asax', '.ascx', '.ashx', '.asmx',
|
|
'.htaccess', '.htpasswd'
|
|
];
|
|
|
|
function sanitizeInput(str) {
|
|
if (typeof str !== 'string') return str;
|
|
|
|
let sanitized = str.replace(/[&<>"']/g, (char) => ({
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
}[char]));
|
|
|
|
sanitized = sanitized.replace(/(?:javascript|data|vbscript|expression|on\w+)\s*:/gi, '');
|
|
return sanitized.replace(/\x00/g, '');
|
|
}
|
|
|
|
function decodeHtmlEntities(str) {
|
|
if (typeof str !== 'string') return str;
|
|
|
|
const entityMap = {
|
|
amp: '&',
|
|
lt: '<',
|
|
gt: '>',
|
|
quot: '"',
|
|
apos: "'",
|
|
'#x27': "'",
|
|
'#x2F': '/',
|
|
'#x60': '`'
|
|
};
|
|
|
|
const decodeOnce = (input) =>
|
|
input.replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z]+);/g, (match, code) => {
|
|
if (code[0] === '#') {
|
|
const isHex = code[1]?.toLowerCase() === 'x';
|
|
const num = isHex ? parseInt(code.slice(2), 16) : parseInt(code.slice(1), 10);
|
|
return Number.isNaN(num) ? match : String.fromCharCode(num);
|
|
}
|
|
const mapped = entityMap[code];
|
|
return mapped !== undefined ? mapped : match;
|
|
});
|
|
|
|
let output = str;
|
|
let decoded = decodeOnce(output);
|
|
while (decoded !== output) {
|
|
output = decoded;
|
|
decoded = decodeOnce(output);
|
|
}
|
|
return output;
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
if (typeof str !== 'string') return str;
|
|
return str.replace(/[&<>"']/g, (char) => ({
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
}[char]));
|
|
}
|
|
|
|
function isSafePathSegment(name) {
|
|
return (
|
|
typeof name === 'string' &&
|
|
name.length > 0 &&
|
|
name.length <= 255 &&
|
|
!name.includes('..') &&
|
|
!/[/\\]/.test(name) &&
|
|
!/[\x00-\x1F]/.test(name)
|
|
);
|
|
}
|
|
|
|
function isFileExtensionSafe(filename) {
|
|
if (!filename || typeof filename !== 'string') return false;
|
|
|
|
const ext = path.extname(filename).toLowerCase();
|
|
const nameLower = filename.toLowerCase();
|
|
if (DANGEROUS_EXTENSIONS.includes(ext)) return false;
|
|
if (['.htaccess', '.htpasswd'].includes(nameLower)) return false;
|
|
|
|
return !DANGEROUS_EXTENSIONS.some((dangerExt) => nameLower.includes(`${dangerExt}.`));
|
|
}
|
|
|
|
module.exports = {
|
|
sanitizeInput,
|
|
decodeHtmlEntities,
|
|
escapeHtml,
|
|
isSafePathSegment,
|
|
isFileExtensionSafe
|
|
};
|