Files
vue-driven-cloud-storage/backend/test_share_edge_cases.js

615 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 分享功能边界条件深度测试(兼容 Cookie + CSRF
*
* 测试场景:
* 1. 已过期的分享
* 2. 分享文件不存在
* 3. 被封禁用户的分享
* 4. 路径遍历攻击
* 5. 特殊字符路径
* 6. 并发密码尝试
* 7. 分享统计
* 8. 分享码唯一性
* 9. 过期时间格式
*/
const http = require('http');
const https = require('https');
const bcrypt = require('bcryptjs');
const { db, ShareDB, UserDB } = require('./database');
const BASE_URL = process.env.TEST_BASE_URL || 'http://127.0.0.1:40001';
const results = {
passed: 0,
failed: 0,
errors: []
};
const adminSession = {
cookies: {},
csrfToken: ''
};
let adminUserId = 1;
function makeCookieHeader(cookies) {
return Object.entries(cookies || {})
.map(([k, v]) => `${k}=${v}`)
.join('; ');
}
function storeSetCookies(session, setCookieHeader) {
if (!session || !setCookieHeader) return;
const list = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader];
for (const raw of list) {
const first = String(raw || '').split(';')[0];
const idx = first.indexOf('=');
if (idx <= 0) continue;
const key = first.slice(0, idx).trim();
const value = first.slice(idx + 1).trim();
session.cookies[key] = value;
if (key === 'csrf_token') {
session.csrfToken = value;
}
}
}
function isSafeMethod(method) {
const upper = String(method || '').toUpperCase();
return upper === 'GET' || upper === 'HEAD' || upper === 'OPTIONS';
}
function request(method, path, options = {}) {
const {
data = null,
session = null,
headers = {},
requireCsrf = true
} = options;
return new Promise((resolve, reject) => {
const url = new URL(path, BASE_URL);
const transport = url.protocol === 'https:' ? https : http;
const requestHeaders = { ...headers };
if (session) {
const cookieHeader = makeCookieHeader(session.cookies);
if (cookieHeader) {
requestHeaders.Cookie = cookieHeader;
}
if (requireCsrf && !isSafeMethod(method)) {
const csrfToken = session.csrfToken || session.cookies.csrf_token;
if (csrfToken) {
requestHeaders['X-CSRF-Token'] = csrfToken;
}
}
}
let payload = null;
if (data !== null && data !== undefined) {
payload = JSON.stringify(data);
requestHeaders['Content-Type'] = 'application/json';
requestHeaders['Content-Length'] = Buffer.byteLength(payload);
}
const req = transport.request({
protocol: url.protocol,
hostname: url.hostname,
port: url.port || (url.protocol === 'https:' ? 443 : 80),
path: url.pathname + url.search,
method,
headers: requestHeaders
}, (res) => {
let body = '';
res.on('data', chunk => {
body += chunk;
});
res.on('end', () => {
if (session) {
storeSetCookies(session, res.headers['set-cookie']);
}
let parsed = body;
try {
parsed = body ? JSON.parse(body) : {};
} catch (e) {
// keep raw text
}
resolve({
status: res.statusCode,
data: parsed,
headers: res.headers
});
});
});
req.on('error', reject);
if (payload) {
req.write(payload);
}
req.end();
});
}
async function initCsrf(session) {
const res = await request('GET', '/api/csrf-token', {
session,
requireCsrf: false
});
if (res.status === 200 && res.data && res.data.csrfToken) {
session.csrfToken = res.data.csrfToken;
}
return res;
}
function assert(condition, message) {
if (condition) {
results.passed++;
console.log(` [PASS] ${message}`);
} else {
results.failed++;
results.errors.push(message);
console.log(` [FAIL] ${message}`);
}
}
function createValidShareCode(prefix = '') {
for (let i = 0; i < 20; i++) {
const generated = ShareDB.generateShareCode();
const code = (prefix + generated).replace(/[^A-Za-z0-9]/g, '').slice(0, 16);
if (!code || code.length < 6) continue;
const exists = db.prepare('SELECT 1 FROM shares WHERE share_code = ?').get(code);
if (!exists) return code;
}
return ShareDB.generateShareCode();
}
async function testExpiredShare() {
console.log('\n[测试] 已过期的分享...');
const expiredShareCode = createValidShareCode('E');
try {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const expiresAt = yesterday.toISOString().replace('T', ' ').substring(0, 19);
db.prepare(`
INSERT INTO shares (user_id, share_code, share_path, share_type, storage_type, expires_at)
VALUES (?, ?, ?, ?, ?, ?)
`).run(adminUserId, expiredShareCode, '/expired-test.txt', 'file', 'local', expiresAt);
console.log(` 创建过期分享: ${expiredShareCode}, 过期时间: ${expiresAt}`);
const res = await request('POST', `/api/share/${expiredShareCode}/verify`, {
data: {},
requireCsrf: false
});
assert(res.status === 404, `过期分享应返回 404, 实际: ${res.status}`);
assert(res.data && res.data.message === '分享不存在', '应提示分享不存在');
db.prepare('DELETE FROM shares WHERE share_code = ?').run(expiredShareCode);
return true;
} catch (error) {
console.log(` [ERROR] ${error.message}`);
results.failed++;
db.prepare('DELETE FROM shares WHERE share_code = ?').run(expiredShareCode);
return false;
}
}
async function testShareWithDeletedFile() {
console.log('\n[测试] 分享的文件不存在...');
const shareCode = createValidShareCode('N');
try {
db.prepare(`
INSERT INTO shares (user_id, share_code, share_path, share_type, storage_type)
VALUES (?, ?, ?, ?, ?)
`).run(adminUserId, shareCode, '/non_existent_file_xyz.txt', 'file', 'local');
console.log(` 创建分享: ${shareCode}, 路径: /non_existent_file_xyz.txt`);
const res = await request('POST', `/api/share/${shareCode}/verify`, {
data: {},
requireCsrf: false
});
assert(res.status === 500 || res.status === 200, `应返回500或200实际: ${res.status}`);
if (res.status === 500) {
assert(!!res.data?.message, '500时应返回错误消息');
}
db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode);
return true;
} catch (error) {
console.log(` [ERROR] ${error.message}`);
db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode);
return false;
}
}
async function testShareByBannedUser() {
console.log('\n[测试] 被封禁用户的分享...');
let testUserId = null;
const shareCode = createValidShareCode('B');
try {
testUserId = UserDB.create({
username: `test_banned_${Date.now()}`,
email: `test_banned_${Date.now()}@test.com`,
password: 'test123',
is_verified: 1
});
db.prepare(`
INSERT INTO shares (user_id, share_code, share_path, share_type, storage_type)
VALUES (?, ?, ?, ?, ?)
`).run(testUserId, shareCode, '/test.txt', 'file', 'local');
console.log(` 创建测试用户 ID: ${testUserId}`);
console.log(` 创建分享: ${shareCode}`);
UserDB.setBanStatus(testUserId, true);
console.log(` 封禁用户: ${testUserId}`);
const res = await request('POST', `/api/share/${shareCode}/verify`, {
data: {},
requireCsrf: false
});
console.log(` 被封禁用户分享访问状态码: ${res.status}`);
db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode);
UserDB.delete(testUserId);
assert(true, '被封禁用户分享测试完成');
return true;
} catch (error) {
console.log(` [ERROR] ${error.message}`);
if (shareCode) db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode);
if (testUserId) UserDB.delete(testUserId);
return false;
}
}
async function testPathTraversalAttacks() {
console.log('\n[测试] 路径遍历攻击防护...');
const shareCode = createValidShareCode('T');
try {
db.prepare(`
INSERT INTO shares (user_id, share_code, share_path, share_type, storage_type)
VALUES (?, ?, ?, ?, ?)
`).run(adminUserId, shareCode, '/allowed-folder', 'directory', 'local');
const attackPaths = [
'../../../etc/passwd',
'..\\..\\..\\etc\\passwd',
'%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd',
'/allowed-folder/../../../etc/passwd',
'/allowed-folder/./../../etc/passwd',
'....//....//....//etc/passwd',
'/allowed-folder%00.txt/../../../etc/passwd'
];
let blocked = 0;
for (const attackPath of attackPaths) {
const res = await request('POST', `/api/share/${shareCode}/download-url`, {
data: { path: attackPath },
requireCsrf: false
});
if (res.status === 403 || res.status === 400) {
blocked++;
console.log(` [BLOCKED] ${attackPath.substring(0, 40)}...`);
} else {
console.log(` [WARN] 可能未阻止: ${attackPath}, 状态: ${res.status}`);
}
}
assert(blocked >= attackPaths.length - 1, `路径遍历攻击应被阻止 (${blocked}/${attackPaths.length})`);
db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode);
return true;
} catch (error) {
console.log(` [ERROR] ${error.message}`);
db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode);
return false;
}
}
async function testSpecialCharactersInPath() {
console.log('\n[测试] 特殊字符路径处理...');
if (!adminSession.cookies.token) {
console.log(' [WARN] 未登录,跳过特殊字符路径测试');
assert(true, '未登录时跳过特殊字符路径测试');
return true;
}
const specialPaths = [
'/文件夹/中文文件.txt',
'/folder with spaces/file.txt',
'/folder-with-dashes/file_underscore.txt',
'/folder.with.dots/file.name.ext.txt',
"/folder'with'quotes/file.txt"
];
let handled = 0;
for (const virtualPath of specialPaths) {
try {
const createRes = await request('POST', '/api/share/create', {
data: {
share_type: 'file',
file_path: virtualPath
},
session: adminSession
});
if (createRes.status === 200 || createRes.status === 400) {
handled++;
console.log(` [OK] ${virtualPath.substring(0, 30)}... - 状态: ${createRes.status}`);
if (createRes.status === 200 && createRes.data?.share_code) {
const mySharesRes = await request('GET', '/api/share/my', {
session: adminSession
});
const share = mySharesRes.data?.shares?.find(s => s.share_code === createRes.data.share_code);
if (share) {
await request('DELETE', `/api/share/${share.id}`, {
session: adminSession
});
}
}
}
} catch (error) {
console.log(` [ERROR] ${virtualPath}: ${error.message}`);
}
}
assert(handled === specialPaths.length, '特殊字符路径处理完成');
return true;
}
async function testConcurrentPasswordAttempts() {
console.log('\n[测试] 并发密码尝试限流...');
const shareCode = createValidShareCode('C');
try {
const hashedPassword = bcrypt.hashSync('correct123', 10);
db.prepare(`
INSERT INTO shares (user_id, share_code, share_path, share_type, share_password, storage_type)
VALUES (?, ?, ?, ?, ?, ?)
`).run(adminUserId, shareCode, '/test.txt', 'file', hashedPassword, 'local');
const promises = [];
for (let i = 0; i < 20; i++) {
promises.push(
request('POST', `/api/share/${shareCode}/verify`, {
data: { password: `wrong${i}` },
requireCsrf: false
})
);
}
const responses = await Promise.all(promises);
const rateLimited = responses.filter(r => r.status === 429).length;
const unauthorized = responses.filter(r => r.status === 401).length;
console.log(` 并发请求: 20, 限流: ${rateLimited}, 401错误: ${unauthorized}`);
if (rateLimited > 0) {
assert(true, '限流机制生效');
} else {
console.log(' [INFO] 限流未触发(可能配置较宽松)');
assert(true, '并发测试完成');
}
db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode);
return true;
} catch (error) {
console.log(` [ERROR] ${error.message}`);
db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode);
return false;
}
}
async function testShareStatistics() {
console.log('\n[测试] 分享统计功能...');
const shareCode = createValidShareCode('S');
try {
db.prepare(`
INSERT INTO shares (user_id, share_code, share_path, share_type, storage_type, view_count, download_count)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(adminUserId, shareCode, '/test.txt', 'file', 'local', 0, 0);
for (let i = 0; i < 3; i++) {
await request('POST', `/api/share/${shareCode}/verify`, {
data: {},
requireCsrf: false
});
}
for (let i = 0; i < 2; i++) {
await request('POST', `/api/share/${shareCode}/download`, {
data: {},
requireCsrf: false
});
}
const share = db.prepare('SELECT view_count, download_count FROM shares WHERE share_code = ?').get(shareCode);
assert(share.view_count === 3, `查看次数应为 3, 实际: ${share.view_count}`);
assert(share.download_count === 2, `下载次数应为 2, 实际: ${share.download_count}`);
console.log(` 查看次数: ${share.view_count}, 下载次数: ${share.download_count}`);
db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode);
return true;
} catch (error) {
console.log(` [ERROR] ${error.message}`);
db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode);
return false;
}
}
async function testShareCodeUniqueness() {
console.log('\n[测试] 分享码唯一性...');
try {
const codes = new Set();
for (let i = 0; i < 10; i++) {
const code = ShareDB.generateShareCode();
if (codes.has(code)) {
console.log(` [WARN] 发现重复分享码: ${code}`);
}
codes.add(code);
}
assert(codes.size === 10, `应生成 10 个唯一分享码, 实际: ${codes.size}`);
console.log(` 生成了 ${codes.size} 个唯一分享码`);
const sampleCode = ShareDB.generateShareCode();
assert(sampleCode.length === 8, `分享码长度应为 8, 实际: ${sampleCode.length}`);
assert(/^[a-zA-Z0-9]+$/.test(sampleCode), '分享码应只包含字母数字');
return true;
} catch (error) {
console.log(` [ERROR] ${error.message}`);
return false;
}
}
async function testExpiryTimeFormat() {
console.log('\n[测试] 过期时间格式...');
try {
const testDays = [1, 7, 30, 365];
for (const days of testDays) {
const result = ShareDB.create(adminUserId, {
share_type: 'file',
file_path: `/test_${days}_days.txt`,
expiry_days: days
});
const share = db.prepare('SELECT expires_at FROM shares WHERE share_code = ?').get(result.share_code);
const expiresAt = new Date(share.expires_at);
const now = new Date();
const diffDays = Math.round((expiresAt - now) / (1000 * 60 * 60 * 24));
assert(Math.abs(diffDays - days) <= 1, `${days}天过期应正确设置, 实际差异: ${diffDays}`);
db.prepare('DELETE FROM shares WHERE share_code = ?').run(result.share_code);
}
return true;
} catch (error) {
console.log(` [ERROR] ${error.message}`);
return false;
}
}
async function login() {
console.log('\n[准备] 登录获取认证...');
try {
await initCsrf(adminSession);
const res = await request('POST', '/api/login', {
data: {
username: 'admin',
password: 'admin123'
},
session: adminSession,
requireCsrf: false
});
if (res.status === 200 && res.data?.success && adminSession.cookies.token) {
await initCsrf(adminSession);
const profileRes = await request('GET', '/api/user/profile', {
session: adminSession
});
if (profileRes.status === 200 && profileRes.data?.user?.id) {
adminUserId = profileRes.data.user.id;
}
console.log(' 认证成功');
return true;
}
console.log(` 认证失败: status=${res.status}, message=${res.data?.message || 'unknown'}`);
return false;
} catch (error) {
console.log(` [ERROR] ${error.message}`);
return false;
}
}
async function runTests() {
console.log('========================================');
console.log(' 分享功能边界条件深度测试Cookie + CSRF');
console.log('========================================');
const loggedIn = await login();
if (!loggedIn) {
console.log('\n[WARN] 登录失败,部分测试可能无法执行');
}
await testExpiredShare();
await testShareWithDeletedFile();
await testShareByBannedUser();
await testPathTraversalAttacks();
await testSpecialCharactersInPath();
await testConcurrentPasswordAttempts();
await testShareStatistics();
await testShareCodeUniqueness();
await testExpiryTimeFormat();
console.log('\n========================================');
console.log(' 测试结果统计');
console.log('========================================');
console.log(`通过: ${results.passed}`);
console.log(`失败: ${results.failed}`);
if (results.errors.length > 0) {
console.log('\n失败的测试:');
results.errors.forEach((err, i) => {
console.log(` ${i + 1}. ${err}`);
});
}
console.log('\n========================================');
process.exit(results.failed > 0 ? 1 : 0);
}
runTests().catch(error => {
console.error('测试执行失败:', error);
process.exit(1);
});