feat: 创建完整的安全测试
- 添加XSS防护测试(脚本注入、img标签、svg标签、javascript伪协议) - 添加SQL注入防护测试(SQL注入、OR注入、注释注入) - 添加路径遍历防护测试(路径遍历、编码路径遍历) - 添加CSRF防护测试(CSRF令牌、令牌验证) - 添加安全头测试(X-Frame-Options、X-Content-Type-Options等) - 添加HTTPS强制跳转测试 - 添加输入验证测试(邮箱格式、手机号格式、必填字段) - 添加敏感数据保护测试(页面源码、控制台日志)~
This commit is contained in:
@@ -0,0 +1,336 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { ContactPage } from '../../pages/ContactPage';
|
||||||
|
import { HomePage } from '../../pages/HomePage';
|
||||||
|
import { SECURITY_TEST_CASES } from '../../data/test-data';
|
||||||
|
|
||||||
|
test.describe('安全测试', () => {
|
||||||
|
test.describe('XSS防护测试', () => {
|
||||||
|
let contactPage: ContactPage;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
contactPage = new ContactPage(page);
|
||||||
|
await contactPage.goto();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该防止XSS脚本注入', async () => {
|
||||||
|
const payloads = SECURITY_TEST_CASES.xssPayloads;
|
||||||
|
|
||||||
|
for (const payload of payloads) {
|
||||||
|
await contactPage.goto();
|
||||||
|
await contactPage.testXSSInjection(payload);
|
||||||
|
await contactPage.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const pageContent = await contactPage.page.content();
|
||||||
|
expect(pageContent).not.toContain('<script>');
|
||||||
|
expect(pageContent).not.toContain('alert(');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该防止img标签XSS注入', async () => {
|
||||||
|
const payload = '<img src=x onerror=alert("XSS")>';
|
||||||
|
await contactPage.testXSSInjection(payload);
|
||||||
|
await contactPage.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const pageContent = await contactPage.page.content();
|
||||||
|
expect(pageContent).not.toContain('onerror=');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该防止svg标签XSS注入', async () => {
|
||||||
|
const payload = '<svg onload=alert("XSS")>';
|
||||||
|
await contactPage.testXSSInjection(payload);
|
||||||
|
await contactPage.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const pageContent = await contactPage.page.content();
|
||||||
|
expect(pageContent).not.toContain('onload=');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该防止javascript伪协议注入', async () => {
|
||||||
|
const payload = 'javascript:alert("XSS")';
|
||||||
|
await contactPage.testXSSInjection(payload);
|
||||||
|
await contactPage.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const pageContent = await contactPage.page.content();
|
||||||
|
expect(pageContent).not.toContain('javascript:');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('SQL注入防护测试', () => {
|
||||||
|
let contactPage: ContactPage;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
contactPage = new ContactPage(page);
|
||||||
|
await contactPage.goto();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该防止SQL注入攻击', async () => {
|
||||||
|
const payloads = SECURITY_TEST_CASES.sqlInjectionPayloads;
|
||||||
|
|
||||||
|
for (const payload of payloads) {
|
||||||
|
await contactPage.goto();
|
||||||
|
await contactPage.testSQLInjection(payload);
|
||||||
|
await contactPage.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const pageContent = await contactPage.page.content();
|
||||||
|
expect(pageContent).not.toContain('DROP TABLE');
|
||||||
|
expect(pageContent).not.toContain('UNION SELECT');
|
||||||
|
expect(pageContent).not.toContain('INSERT INTO');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该防止OR注入攻击', async () => {
|
||||||
|
const payload = "' OR '1'='1";
|
||||||
|
await contactPage.testSQLInjection(payload);
|
||||||
|
await contactPage.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const pageContent = await contactPage.page.content();
|
||||||
|
expect(pageContent).not.toContain("' OR '");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该防止注释注入攻击', async () => {
|
||||||
|
const payload = "'; DROP TABLE users; --";
|
||||||
|
await contactPage.testSQLInjection(payload);
|
||||||
|
await contactPage.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const pageContent = await contactPage.page.content();
|
||||||
|
expect(pageContent).not.toContain('DROP TABLE');
|
||||||
|
expect(pageContent).not.toContain('--');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('路径遍历防护测试', () => {
|
||||||
|
let contactPage: ContactPage;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
contactPage = new ContactPage(page);
|
||||||
|
await contactPage.goto();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该防止路径遍历攻击', async () => {
|
||||||
|
const payloads = SECURITY_TEST_CASES.pathTraversalPayloads;
|
||||||
|
|
||||||
|
for (const payload of payloads) {
|
||||||
|
await contactPage.goto();
|
||||||
|
await contactPage.testPathTraversal(payload);
|
||||||
|
await contactPage.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const pageContent = await contactPage.page.content();
|
||||||
|
expect(pageContent).not.toContain('../');
|
||||||
|
expect(pageContent).not.toContain('..\\');
|
||||||
|
expect(pageContent).not.toContain('etc/passwd');
|
||||||
|
expect(pageContent).not.toContain('windows\\system32');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该防止编码路径遍历攻击', async () => {
|
||||||
|
const payload = '....//....//....//etc/passwd';
|
||||||
|
await contactPage.testPathTraversal(payload);
|
||||||
|
await contactPage.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const pageContent = await contactPage.page.content();
|
||||||
|
expect(pageContent).not.toContain('etc/passwd');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('CSRF防护测试', () => {
|
||||||
|
let homePage: HomePage;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
homePage = new HomePage(page);
|
||||||
|
await homePage.goto();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该包含CSRF令牌', async () => {
|
||||||
|
await homePage.clickContactButton();
|
||||||
|
await homePage.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
const csrfToken = await homePage.page.evaluate(() => {
|
||||||
|
const token = document.querySelector('input[name="csrf_token"]');
|
||||||
|
return token ? (token as HTMLInputElement).value : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(csrfToken).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该验证CSRF令牌', async () => {
|
||||||
|
await homePage.clickContactButton();
|
||||||
|
await homePage.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
const hasToken = await homePage.page.evaluate(() => {
|
||||||
|
const form = document.querySelector('form');
|
||||||
|
return form ? form.querySelector('input[name="csrf_token"]') !== null : false;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(hasToken).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('安全头测试', () => {
|
||||||
|
let homePage: HomePage;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
homePage = new HomePage(page);
|
||||||
|
await homePage.goto();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该包含X-Frame-Options头', async () => {
|
||||||
|
const response = await homePage.page.evaluate(async () => {
|
||||||
|
const response = await fetch(window.location.href);
|
||||||
|
return response.headers.get('X-Frame-Options');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该包含X-Content-Type-Options头', async () => {
|
||||||
|
const response = await homePage.page.evaluate(async () => {
|
||||||
|
const response = await fetch(window.location.href);
|
||||||
|
return response.headers.get('X-Content-Type-Options');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response).toBe('nosniff');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该包含Content-Security-Policy头', async () => {
|
||||||
|
const response = await homePage.page.evaluate(async () => {
|
||||||
|
const response = await fetch(window.location.href);
|
||||||
|
return response.headers.get('Content-Security-Policy');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该包含Strict-Transport-Security头', async () => {
|
||||||
|
const response = await homePage.page.evaluate(async () => {
|
||||||
|
const response = await fetch(window.location.href);
|
||||||
|
return response.headers.get('Strict-Transport-Security');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('HTTPS强制跳转测试', () => {
|
||||||
|
test('应该强制使用HTTPS', async ({ page, context }) => {
|
||||||
|
const baseURL = process.env.TEST_ENV === 'production'
|
||||||
|
? 'https://novalon.com'
|
||||||
|
: process.env.TEST_ENV === 'staging'
|
||||||
|
? 'https://staging.novalon.com'
|
||||||
|
: 'http://localhost:3001';
|
||||||
|
|
||||||
|
await page.goto(baseURL);
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
const currentURL = page.url();
|
||||||
|
if (baseURL.startsWith('http://') && !baseURL.includes('localhost')) {
|
||||||
|
expect(currentURL).toMatch(/^https:\/\//);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('输入验证测试', () => {
|
||||||
|
let contactPage: ContactPage;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
contactPage = new ContactPage(page);
|
||||||
|
await contactPage.goto();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该验证邮箱格式', async () => {
|
||||||
|
const invalidEmails = ['invalid-email', '@example.com', 'user@', 'user@domain'];
|
||||||
|
|
||||||
|
for (const email of invalidEmails) {
|
||||||
|
await contactPage.goto();
|
||||||
|
await contactPage.fillContactForm({
|
||||||
|
name: '测试用户',
|
||||||
|
email: email,
|
||||||
|
phone: '13800138000',
|
||||||
|
message: '测试消息',
|
||||||
|
});
|
||||||
|
await contactPage.submitForm();
|
||||||
|
await contactPage.waitForTimeout(1000);
|
||||||
|
|
||||||
|
const isEmailErrorVisible = await contactPage.isEmailErrorVisible();
|
||||||
|
expect(isEmailErrorVisible).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该验证手机号格式', async () => {
|
||||||
|
const invalidPhones = ['123', '123456789012345', 'abcdefghijk'];
|
||||||
|
|
||||||
|
for (const phone of invalidPhones) {
|
||||||
|
await contactPage.goto();
|
||||||
|
await contactPage.fillContactForm({
|
||||||
|
name: '测试用户',
|
||||||
|
email: 'test@example.com',
|
||||||
|
phone: phone,
|
||||||
|
message: '测试消息',
|
||||||
|
});
|
||||||
|
await contactPage.submitForm();
|
||||||
|
await contactPage.waitForTimeout(1000);
|
||||||
|
|
||||||
|
const isPhoneErrorVisible = await contactPage.isPhoneErrorVisible();
|
||||||
|
expect(isPhoneErrorVisible).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该验证必填字段', async () => {
|
||||||
|
await contactPage.fillContactForm({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
message: '',
|
||||||
|
});
|
||||||
|
await contactPage.submitForm();
|
||||||
|
await contactPage.waitForTimeout(1000);
|
||||||
|
|
||||||
|
const requiredFields = await contactPage.verifyRequiredFields();
|
||||||
|
expect(requiredFields.nameRequired).toBe(true);
|
||||||
|
expect(requiredFields.emailRequired).toBe(true);
|
||||||
|
expect(requiredFields.phoneRequired).toBe(true);
|
||||||
|
expect(requiredFields.messageRequired).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('敏感数据保护测试', () => {
|
||||||
|
let contactPage: ContactPage;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
contactPage = new ContactPage(page);
|
||||||
|
await contactPage.goto();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该不在页面源码中暴露敏感数据', async () => {
|
||||||
|
const pageContent = await contactPage.page.content();
|
||||||
|
|
||||||
|
expect(pageContent).not.toContain('password');
|
||||||
|
expect(pageContent).not.toContain('api_key');
|
||||||
|
expect(pageContent).not.toContain('secret');
|
||||||
|
expect(pageContent).not.toContain('token');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该不在控制台日志中暴露敏感数据', async () => {
|
||||||
|
const logs: string[] = [];
|
||||||
|
contactPage.page.on('console', msg => {
|
||||||
|
logs.push(msg.text());
|
||||||
|
});
|
||||||
|
|
||||||
|
await contactPage.fillContactForm({
|
||||||
|
name: '测试用户',
|
||||||
|
email: 'test@example.com',
|
||||||
|
phone: '13800138000',
|
||||||
|
message: '测试消息',
|
||||||
|
});
|
||||||
|
await contactPage.submitForm();
|
||||||
|
await contactPage.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const sensitiveDataFound = logs.some(log =>
|
||||||
|
log.includes('password') ||
|
||||||
|
log.includes('api_key') ||
|
||||||
|
log.includes('secret') ||
|
||||||
|
log.includes('token')
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(sensitiveDataFound).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user