name: security-testing description: 通过SAST、DAST、渗透测试和依赖扫描识别安全漏洞。用于安全测试、漏洞扫描、OWASP、SQL注入、XSS、CSRF和渗透测试。
安全测试
概览
安全测试识别应用程序中的漏洞、弱点和威胁,以确保数据保护、防止未经授权的访问并维护系统完整性。它结合了自动化扫描(SAST、DAST)与手动渗透测试和代码审查。
何时使用
- 测试OWASP Top 10漏洞
- 扫描依赖项以查找已知漏洞
- 测试认证和授权
- 验证输入清理
- 测试API安全性
- 检查敏感数据暴露
- 验证安全头部
- 测试会话管理
安全测试类型
- SAST:静态应用安全测试(代码分析)
- DAST:动态应用安全测试(运行时)
- IAST:交互式应用安全测试
- SCA:软件组件分析(依赖项)
- 渗透测试:手动安全测试
- 模糊测试:无效/随机输入测试
指令
1. OWASP ZAP (DAST)
# security_scan.py
from zapv2 import ZAPv2
import time
class SecurityScanner:
def __init__(self, target_url, api_key=None):
self.zap = ZAPv2(apikey=api_key, proxies={
'http': 'http://localhost:8080',
'https': 'http://localhost:8080'
})
self.target = target_url
def scan(self):
"""运行完整安全扫描。"""
print(f"扫描 {self.target}...")
# 爬取应用程序
print("爬取中...")
scan_id = self.zap.spider.scan(self.target)
while int(self.zap.spider.status(scan_id)) < 100:
time.sleep(2)
print(f"爬取进度:{self.zap.spider.status(scan_id)}%")
# 活跃扫描
print("运行活跃扫描...")
scan_id = self.zap.ascan.scan(self.target)
while int(self.zap.ascan.status(scan_id)) < 100:
time.sleep(5)
print(f"扫描进度:{self.zap.ascan.status(scan_id)}%")
return self.get_results()
def get_results(self):
"""获取扫描结果。"""
alerts = self.zap.core.alerts(baseurl=self.target)
# 按风险等级分组
results = {
'high': [],
'medium': [],
'low': [],
'informational': []
}
for alert in alerts:
risk = alert['risk'].lower()
results[risk].append({
'name': alert['alert'],
'description': alert['description'],
'solution': alert['solution'],
'url': alert['url'],
'param': alert.get('param', ''),
'evidence': alert.get('evidence', '')
})
return results
def report(self, results):
"""生成安全报告。"""
print("
" + "="*60)
print("安全扫描结果")
print("="*60)
for risk_level in ['high', 'medium', 'low', 'informational']:
issues = results[risk_level]
if issues:
print(f"
{risk_level.upper()} 风险问题:{len(issues)}")
for issue in issues[:5]: # 显示前5个
print(f" - {issue['name']}")
print(f" URL: {issue['url']}")
if issue['param']:
print(f" 参数:{issue['param']}")
# 如果发现高风险,失败
if results['high']:
raise Exception(f"发现 {len(results['high'])} 个高风险漏洞!")
# 使用方法
scanner = SecurityScanner('http://localhost:3000')
results = scanner.scan()
scanner.report(results)
2. SQL注入测试
// tests/security/sql-injection.test.ts
import { test, expect } from '@playwright/test';
import request from 'supertest';
import { app } from '../../src/app';
test.describe('SQL注入保护', () => {
const sqlInjectionPayloads = [
"' OR '1'='1",
"'; DROP TABLE users; --",
"' UNION SELECT * FROM users --",
"admin'--",
"' OR 1=1--",
"1' AND '1'='1",
];
test('登录应防止SQL注入', async () => {
for (const payload of sqlInjectionPayloads) {
const response = await request(app)
.post('/api/auth/login')
.send({
email: payload,
password: payload,
});
// 应返回400/401,而不是500(SQL错误)
expect([400, 401]).toContain(response.status);
expect(response.body).not.toMatch(/SQL|syntax|error/i);
}
});
test('搜索应清理输入', async () => {
for (const payload of sqlInjectionPayloads) {
const response = await request(app)
.get('/api/products/search')
.query({ q: payload });
// 不应导致SQL错误
expect(response.status).toBeLessThan(500);
expect(response.body).not.toMatch(/SQL|syntax/i);
}
});
test('数字参数应验证', async () => {
const response = await request(app)
.get('/api/users/abc') // 非数字ID
.expect(400);
expect(response.body.error).toBeTruthy();
});
});
3. XSS测试
// tests/security/xss.test.js
describe('XSS保护', () => {
const xssPayloads = [
'<script>alert("XSS")</script>',
'<img src=x onerror=alert("XSS")>',
'<svg onload=alert("XSS")>',
'javascript:alert("XSS")',
'<iframe src="javascript:alert(\'XSS\')">',
'<body onload=alert("XSS")>',
];
test('用户输入应转义', async () => {
const { page } = await browser.newPage();
for (const payload of xssPayloads) {
await page.goto('/');
// 提交带有XSS负载的评论
await page.fill('[name="comment"]', payload);
await page.click('[type="submit"]');
// 等待评论出现
await page.waitForSelector('.comment');
// 检查脚本是否未执行
const dialogAppeared = await page.evaluate(() => {
return window.xssDetected || false;
});
expect(dialogAppeared).toBe(false);
// 检查HTML是否已转义
const commentHTML = await page.$eval('.comment', el => el.innerHTML);
expect(commentHTML).not.toContain('<script>');
expect(commentHTML).toContain('<script>');
}
});
test('URL应验证', async () => {
const response = await request(app)
.post('/api/links')
.send({ url: 'javascript:alert("XSS")' })
.expect(400);
expect(response.body.error).toMatch(/invalid url/i);
});
});
4. 认证与授权测试
// tests/security/auth.test.ts
describe('认证安全', () => {
test('应拒绝弱密码', async () => {
const weakPasswords = [
'password',
'12345678',
'qwerty',
'abc123',
'password123',
];
for (const password of weakPasswords) {
const response = await request(app)
.post('/api/users')
.send({
email: 'test@example.com',
password,
});
expect(response.status).toBe(400);
expect(response.body.error).toMatch(/password.*weak|password.*requirements/i);
}
});
test('应限制登录尝试次数', async () => {
const credentials = {
email: 'test@example.com',
password: 'wrongpassword',
};
// 尝试10次失败的登录
for (let i = 0; i < 10; i++) {
await request(app)
.post('/api/auth/login')
.send(credentials);
}
// 第11次尝试应受到速率限制
const response = await request(app)
.post('/api/auth/login')
.send(credentials);
expect(response.status).toBe(429);
expect(response.body.error).toMatch(/too many attempts|rate limit/i);
});
test('应防止未经授权的访问', async () => {
const response = await request(app)
.get('/api/admin/users')
.expect(401);
});
test('应防止权限提升', async () => {
const regularUserToken = await getRegularUserToken();
const response = await request(app)
.delete('/api/users/999') // 尝试删除另一个用户
.set('Authorization', `Bearer ${regularUserToken}`)
.expect(403);
});
test('JWT令牌应过期', async () => {
// 创建过期令牌
const expiredToken = jwt.sign(
{ userId: '123' },
JWT_SECRET,
{ expiresIn: '-1s' }
);
const response = await request(app)
.get('/api/protected')
.set('Authorization', `Bearer ${expiredToken}`)
.expect(401);
});
});
5. CSRF保护测试
# tests/security/test_csrf.py
import pytest
from flask import session
class TestCSRFProtection:
def test_post_without_csrf_token_rejected(self, client):
"""没有CSRF令牌的POST请求应被拒绝。"""
response = client.post('/api/users', json={
'email': 'test@example.com',
'name': 'Test'
})
assert response.status_code == 403
assert 'CSRF' in response.json['error']
def test_post_with_invalid_csrf_token_rejected(self, client):
"""带有无效CSRF令牌的POST应被拒绝。"""
response = client.post('/api/users',
json={'email': 'test@example.com'},
headers={'X-CSRF-Token': 'invalid-token'}
)
assert response.status_code == 403
def test_post_with_valid_csrf_token_accepted(self, client):
"""带有有效CSRF令牌的POST应被接受。"""
# 获取CSRF令牌
response = client.get('/api/csrf-token')
csrf_token = response.json['csrfToken']
# 在POST中使用令牌
response = client.post('/api/users',
json={'email': 'test@example.com', 'name': 'Test'},
headers={'X-CSRF-Token': csrf_token}
)
assert response.status_code == 201
6. 依赖漏洞扫描
# 运行npm audit
npm audit
# 修复漏洞
npm audit fix
# 对于Python - Safety
pip install safety
safety check
# 对于Java - OWASP Dependency Check
mvn org.owasp:dependency-check-maven:check
# .github/workflows/security.yml
name: 安全扫描
on: [push, pull_request]
jobs:
dependency-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: 运行npm audit
run: npm audit --audit-level=high
- name: 运行Snyk
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
sast-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: 运行Semgrep
uses: returntocorp/semgrep-action@v1
with:
config: >-
p/security-audit
p/owasp-top-ten
dast-scan:
runs-on: ubuntu-latest
steps:
- name: ZAP扫描
uses: zaproxy/action-baseline@v0.7.0
with:
target: 'http://localhost:3000'
7. 安全头部测试
// tests/security/headers.test.ts
test.describe('安全头部', () => {
test('应有必需的安全头部', async () => {
const response = await request(app).get('/');
expect(response.headers).toMatchObject({
'x-frame-options': 'DENY',
'x-content-type-options': 'nosniff',
'x-xss-protection': '1; mode=block',
'strict-transport-security': expect.stringMatching(/max-age=/),
'content-security-policy': expect.any(String),
});
});
test('不应暴露敏感头部', async () => {
const response = await request(app).get('/');
expect(response.headers['x-powered-by']).toBeUndefined();
expect(response.headers['server']).not.toMatch(/express|nginx|apache/i);
});
test('CSP应防止内联脚本', async ({ page }) => {
await page.goto('/');
const cspViolations = [];
page.on('console', msg => {
if (msg.type() === 'error' && msg.text().includes('Content Security Policy')) {
cspViolations.push(msg.text());
}
});
// 尝试注入内联脚本
await page.evaluate(() => {
const script = document.createElement('script');
script.textContent = 'alert("test")';
document.body.appendChild(script);
});
expect(cspViolations.length).toBeGreaterThan(0);
});
});
8. 秘密检测
# 安装detect-secrets
pip install detect-secrets
# 扫描仓库
detect-secrets scan --all-files --force-use-all-plugins
# 检查硬编码的秘密
git secrets --scan
# TruffleHog用于git历史
trufflehog git https://github.com/user/repo --only-verified
OWASP Top 10测试
- 访问控制破坏:测试授权,权限提升
- 密码学失败:检查弱加密,暴露的秘密
- 注入:SQL、NoSQL、命令注入
- 不安全设计:架构缺陷
- 安全配置不当:默认配置,不必要的功能
- 易受攻击的组件:过时的依赖项
- 认证失败:弱密码,会话管理
- 软件和数据完整性:未签名的包,不安全的CI/CD
- 日志记录失败:日志记录不足,日志中的敏感数据
- SSRF:服务器端请求伪造
最佳实践
✅ DO
- 在CI/CD中运行安全扫描
- 使用真实攻击向量进行测试
- 定期扫描依赖项
- 使用安全头部
- 实施速率限制
- 验证和清理所有输入
- 使用参数化查询
- 彻底测试认证/授权
❌ DON’T
- 在代码中存储秘密
- 信任用户输入
- 暴露详细的错误消息
- 跳过依赖项更新
- 使用默认凭据
- 忽略安全警告
- 仅测试快乐路径
- 提交敏感数据
工具
SAST
- Semgrep:多语言静态分析
- SonarQube:代码质量和安全
- Bandit:Python安全linter
- ESLint插件:JavaScript安全
DAST
- OWASP ZAP:Web应用安全扫描器
- Burp Suite:安全测试平台
- Nikto:Web服务器扫描器
SCA
- Snyk:依赖漏洞扫描
- npm audit:Node.js依赖项
- OWASP Dependency-Check:多语言
- Safety:Python依赖项
秘密
- detect-secrets:预提交钩子
- GitGuardian:秘密检测
- TruffleHog:Git历史扫描
渗透测试清单
输入验证
- [ ] 阻止SQL注入尝试
- [ ] 转义XSS负载
- [ ] 防止命令注入
- [ ] 阻止路径遍历
- [ ] 文件上传限制
认证
- [ ] 强密码策略
- [ ] 失败尝试后账户锁定
- [ ] 实施会话超时
- [ ] 安全的密码重置
- [ ] 可用MFA
授权
- [ ] 基于角色的访问控制
- [ ] 防止权限提升
- [ ] 直接对象引用安全
- [ ] API端点受保护
数据保护
- [ ] 加密敏感数据
- [ ] 强制HTTPS
- [ ] 安全Cookie(HttpOnly,Secure)
- [ ] 日志中无秘密
- [ ] 正确处理PII
示例
另见:持续测试、API契约测试、代码审查分析,以获得全面的安全实践。