fix: harden backend stability and test production utilities
This commit is contained in:
@@ -12,6 +12,12 @@
|
||||
const assert = require('assert');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const {
|
||||
sanitizeInput,
|
||||
decodeHtmlEntities,
|
||||
isSafePathSegment,
|
||||
isFileExtensionSafe
|
||||
} = require('../utils/input-security');
|
||||
|
||||
// 主函数包装器(支持 async/await)
|
||||
async function runTests() {
|
||||
@@ -58,28 +64,6 @@ console.log('\n========== 1. 输入边界测试 ==========\n');
|
||||
function testSanitizeInput() {
|
||||
console.log('--- 测试 XSS 过滤函数 sanitizeInput ---');
|
||||
|
||||
// 从 server.js 复制的 sanitizeInput 函数
|
||||
function sanitizeInput(str) {
|
||||
if (typeof str !== 'string') return str;
|
||||
|
||||
let sanitized = str
|
||||
.replace(/[&<>"']/g, (char) => {
|
||||
const map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return map[char];
|
||||
});
|
||||
|
||||
sanitized = sanitized.replace(/(?:javascript|data|vbscript|expression|on\w+)\s*:/gi, '');
|
||||
sanitized = sanitized.replace(/\x00/g, '');
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
// 空字符串测试
|
||||
test('空字符串输入应该返回空字符串', () => {
|
||||
assert.strictEqual(sanitizeInput(''), '');
|
||||
@@ -263,17 +247,6 @@ console.log('\n========== 2. 文件操作边界测试 ==========\n');
|
||||
function testPathSecurity() {
|
||||
console.log('--- 测试路径安全校验 ---');
|
||||
|
||||
function isSafePathSegment(name) {
|
||||
return (
|
||||
typeof name === 'string' &&
|
||||
name.length > 0 &&
|
||||
name.length <= 255 &&
|
||||
!name.includes('..') &&
|
||||
!/[/\\]/.test(name) &&
|
||||
!/[\x00-\x1F]/.test(name)
|
||||
);
|
||||
}
|
||||
|
||||
test('空文件名应该被拒绝', () => {
|
||||
assert.strictEqual(isSafePathSegment(''), false);
|
||||
});
|
||||
@@ -312,32 +285,6 @@ testPathSecurity();
|
||||
function testFileExtensionSecurity() {
|
||||
console.log('\n--- 测试文件扩展名安全 ---');
|
||||
|
||||
const DANGEROUS_EXTENSIONS = [
|
||||
'.php', '.php3', '.php4', '.php5', '.phtml', '.phar',
|
||||
'.jsp', '.jspx', '.jsw', '.jsv', '.jspf',
|
||||
'.asp', '.aspx', '.asa', '.asax', '.ascx', '.ashx', '.asmx',
|
||||
'.htaccess', '.htpasswd'
|
||||
];
|
||||
|
||||
function isFileExtensionSafe(filename) {
|
||||
if (!filename || typeof filename !== 'string') return false;
|
||||
|
||||
const ext = path.extname(filename).toLowerCase();
|
||||
|
||||
if (DANGEROUS_EXTENSIONS.includes(ext)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const nameLower = filename.toLowerCase();
|
||||
for (const dangerExt of DANGEROUS_EXTENSIONS) {
|
||||
if (nameLower.includes(dangerExt + '.')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
test('PHP 文件应该被拒绝', () => {
|
||||
assert.strictEqual(isFileExtensionSafe('test.php'), false);
|
||||
assert.strictEqual(isFileExtensionSafe('shell.phtml'), false);
|
||||
@@ -360,36 +307,8 @@ function testFileExtensionSecurity() {
|
||||
});
|
||||
|
||||
test('.htaccess 和 .htpasswd 文件应该被拒绝', () => {
|
||||
// 更新测试以匹配修复后的 isFileExtensionSafe 函数
|
||||
// 现在会检查 dangerousFilenames 列表
|
||||
const dangerousFilenames = ['.htaccess', '.htpasswd'];
|
||||
|
||||
function isFileExtensionSafeFixed(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 (dangerousFilenames.includes(nameLower)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const dangerExt of DANGEROUS_EXTENSIONS) {
|
||||
if (nameLower.includes(dangerExt + '.')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
assert.strictEqual(isFileExtensionSafeFixed('.htaccess'), false);
|
||||
assert.strictEqual(isFileExtensionSafeFixed('.htpasswd'), false);
|
||||
assert.strictEqual(isFileExtensionSafe('.htaccess'), false);
|
||||
assert.strictEqual(isFileExtensionSafe('.htpasswd'), false);
|
||||
});
|
||||
|
||||
test('正常文件应该被接受', () => {
|
||||
@@ -771,43 +690,6 @@ console.log('\n========== 7. HTML 实体解码测试 ==========\n');
|
||||
function testHtmlEntityDecoding() {
|
||||
console.log('--- 测试 HTML 实体解码 ---');
|
||||
|
||||
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);
|
||||
if (!Number.isNaN(num)) {
|
||||
return String.fromCharCode(num);
|
||||
}
|
||||
return match;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
test('基本 HTML 实体应该被解码', () => {
|
||||
assert.strictEqual(decodeHtmlEntities('<'), '<');
|
||||
assert.strictEqual(decodeHtmlEntities('>'), '>');
|
||||
|
||||
Reference in New Issue
Block a user