- 发布于
Web安全最佳实践:构建安全可靠的前端应用
- 作者

- 姓名
- 全能波
- GitHub
- @weicracker
Web安全最佳实践:构建安全可靠的前端应用
Web安全是现代应用开发的重要基石。本文将深入探讨前端安全的核心威胁和防护策略,帮助开发者构建更安全的Web应用。
XSS攻击防护
XSS攻击类型与防护
// XSS防护工具类
class XSSProtection {
constructor() {
this.htmlEntities = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
'`': '`',
'=': '='
}
this.allowedTags = new Set([
'p', 'br', 'strong', 'em', 'u', 'i', 'b',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'ul', 'ol', 'li', 'blockquote', 'code', 'pre'
])
this.allowedAttributes = new Map([
['a', new Set(['href', 'title'])],
['img', new Set(['src', 'alt', 'title'])],
['*', new Set(['class', 'id'])]
])
}
// HTML实体编码
escapeHtml(text) {
if (typeof text !== 'string') {
return text
}
return text.replace(/[&<>"'`=\/]/g, (match) => {
return this.htmlEntities[match]
})
}
// HTML实体解码
unescapeHtml(text) {
if (typeof text !== 'string') {
return text
}
const entityMap = Object.fromEntries(
Object.entries(this.htmlEntities).map(([key, value]) => [value, key])
)
return text.replace(/&|<|>|"|'|/|`|=/g, (match) => {
return entityMap[match] || match
})
}
// 安全的innerHTML设置
safeInnerHTML(element, html) {
// 清理HTML内容
const cleanHtml = this.sanitizeHtml(html)
// 使用DOMPurify或自定义清理
element.innerHTML = cleanHtml
// 移除所有脚本标签
const scripts = element.querySelectorAll('script')
scripts.forEach(script => script.remove())
// 移除事件处理器属性
this.removeEventHandlers(element)
}
// HTML清理
sanitizeHtml(html) {
// 创建临时DOM元素
const temp = document.createElement('div')
temp.innerHTML = html
// 递归清理所有节点
this.cleanNode(temp)
return temp.innerHTML
}
// 清理DOM节点
cleanNode(node) {
// 移除不允许的标签
const children = Array.from(node.children)
children.forEach(child => {
const tagName = child.tagName.toLowerCase()
if (!this.allowedTags.has(tagName)) {
// 移除标签但保留内容
const textContent = child.textContent
const textNode = document.createTextNode(textContent)
child.parentNode.replaceChild(textNode, child)
return
}
// 清理属性
this.cleanAttributes(child)
// 递归清理子节点
this.cleanNode(child)
})
}
// 清理属性
cleanAttributes(element) {
const tagName = element.tagName.toLowerCase()
const allowedAttrs = this.allowedAttributes.get(tagName) || new Set()
const globalAttrs = this.allowedAttributes.get('*') || new Set()
const attributes = Array.from(element.attributes)
attributes.forEach(attr => {
const attrName = attr.name.toLowerCase()
// 移除事件处理器
if (attrName.startsWith('on')) {
element.removeAttribute(attr.name)
return
}
// 移除javascript:协议
if (attr.value && attr.value.toLowerCase().includes('javascript:')) {
element.removeAttribute(attr.name)
return
}
// 检查是否为允许的属性
if (!allowedAttrs.has(attrName) && !globalAttrs.has(attrName)) {
element.removeAttribute(attr.name)
}
})
}
// 移除事件处理器
removeEventHandlers(element) {
const allElements = [element, ...element.querySelectorAll('*')]
allElements.forEach(el => {
// 移除所有事件监听器属性
const attributes = Array.from(el.attributes)
attributes.forEach(attr => {
if (attr.name.toLowerCase().startsWith('on')) {
el.removeAttribute(attr.name)
}
})
// 清理可能的脚本内容
if (el.tagName === 'SCRIPT') {
el.remove()
}
})
}
// 安全的URL验证
isValidUrl(url) {
try {
const urlObj = new URL(url)
// 只允许http和https协议
if (!['http:', 'https:'].includes(urlObj.protocol)) {
return false
}
// 检查是否包含危险字符
const dangerousPatterns = [
/javascript:/i,
/data:/i,
/vbscript:/i,
/onload=/i,
/onerror=/i
]
return !dangerousPatterns.some(pattern => pattern.test(url))
} catch {
return false
}
}
// 安全的JSON解析
safeJsonParse(jsonString, defaultValue = null) {
try {
// 检查是否包含危险内容
if (this.containsDangerousContent(jsonString)) {
console.warn('Potentially dangerous content detected in JSON')
return defaultValue
}
return JSON.parse(jsonString)
} catch (error) {
console.error('JSON parse error:', error)
return defaultValue
}
}
// 检查危险内容
containsDangerousContent(content) {
const dangerousPatterns = [
/<script/i,
/javascript:/i,
/on\w+\s*=/i,
/<iframe/i,
/<object/i,
/<embed/i,
/eval\s*\(/i,
/Function\s*\(/i
]
return dangerousPatterns.some(pattern => pattern.test(content))
}
// 内容安全策略助手
generateCSPHeader() {
const directives = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com",
"img-src 'self' data: https:",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'"
]
return directives.join('; ')
}
}
// 输入验证器
class InputValidator {
constructor() {
this.patterns = {
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
phone: /^\+?[\d\s\-\(\)]+$/,
url: /^https?:\/\/.+/,
alphanumeric: /^[a-zA-Z0-9]+$/,
username: /^[a-zA-Z0-9_]{3,20}$/,
password: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/
}
this.maxLengths = {
name: 100,
email: 254,
message: 5000,
title: 200,
description: 1000
}
}
// 验证输入
validate(input, rules) {
const errors = []
for (const [field, value] of Object.entries(input)) {
const fieldRules = rules[field]
if (!fieldRules) continue
// 必填验证
if (fieldRules.required && (!value || value.toString().trim() === '')) {
errors.push(`${field} is required`)
continue
}
// 跳过空值的其他验证
if (!value) continue
// 类型验证
if (fieldRules.type && !this.validateType(value, fieldRules.type)) {
errors.push(`${field} must be a valid ${fieldRules.type}`)
}
// 长度验证
if (fieldRules.maxLength && value.toString().length > fieldRules.maxLength) {
errors.push(`${field} must be less than ${fieldRules.maxLength} characters`)
}
if (fieldRules.minLength && value.toString().length < fieldRules.minLength) {
errors.push(`${field} must be at least ${fieldRules.minLength} characters`)
}
// 自定义验证
if (fieldRules.custom && !fieldRules.custom(value)) {
errors.push(`${field} is invalid`)
}
}
return {
isValid: errors.length === 0,
errors
}
}
// 类型验证
validateType(value, type) {
const pattern = this.patterns[type]
if (!pattern) return true
return pattern.test(value.toString())
}
// 清理输入
sanitizeInput(input) {
if (typeof input !== 'string') {
return input
}
return input
.trim()
.replace(/\s+/g, ' ') // 合并多个空格
.slice(0, 10000) // 限制最大长度
}
// 验证文件上传
validateFile(file, options = {}) {
const {
maxSize = 5 * 1024 * 1024, // 5MB
allowedTypes = ['image/jpeg', 'image/png', 'image/gif'],
allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif']
} = options
const errors = []
// 文件大小验证
if (file.size > maxSize) {
errors.push(`File size must be less than ${maxSize / 1024 / 1024}MB`)
}
// 文件类型验证
if (!allowedTypes.includes(file.type)) {
errors.push(`File type ${file.type} is not allowed`)
}
// 文件扩展名验证
const extension = '.' + file.name.split('.').pop().toLowerCase()
if (!allowedExtensions.includes(extension)) {
errors.push(`File extension ${extension} is not allowed`)
}
// 文件名验证
if (!/^[a-zA-Z0-9._-]+$/.test(file.name)) {
errors.push('File name contains invalid characters')
}
return {
isValid: errors.length === 0,
errors
}
}
}
// 安全的DOM操作
class SafeDOM {
static setText(element, text) {
element.textContent = text
}
static setHTML(element, html) {
const xssProtection = new XSSProtection()
xssProtection.safeInnerHTML(element, html)
}
static setAttribute(element, name, value) {
// 验证属性名
if (!/^[a-zA-Z-]+$/.test(name)) {
console.warn('Invalid attribute name:', name)
return
}
// 验证属性值
if (name.toLowerCase().startsWith('on')) {
console.warn('Event handler attributes are not allowed:', name)
return
}
if (typeof value === 'string' && value.toLowerCase().includes('javascript:')) {
console.warn('JavaScript URLs are not allowed:', value)
return
}
element.setAttribute(name, value)
}
static createElement(tagName, attributes = {}, textContent = '') {
const element = document.createElement(tagName)
// 设置属性
Object.entries(attributes).forEach(([name, value]) => {
this.setAttribute(element, name, value)
})
// 设置文本内容
if (textContent) {
this.setText(element, textContent)
}
return element
}
static createLink(href, text, attributes = {}) {
const xssProtection = new XSSProtection()
if (!xssProtection.isValidUrl(href)) {
console.warn('Invalid URL:', href)
return this.createElement('span', {}, text)
}
return this.createElement('a', {
href,
rel: 'noopener noreferrer',
...attributes
}, text)
}
}
// 使用示例
const xssProtection = new XSSProtection()
const inputValidator = new InputValidator()
// 表单验证示例
function validateForm(formData) {
const rules = {
name: {
required: true,
maxLength: 100,
custom: (value) => !/[<>]/.test(value)
},
email: {
required: true,
type: 'email',
maxLength: 254
},
message: {
required: true,
maxLength: 5000,
custom: (value) => !xssProtection.containsDangerousContent(value)
}
}
return inputValidator.validate(formData, rules)
}
// 安全的内容渲染
function renderUserContent(content, container) {
// 清理内容
const cleanContent = xssProtection.sanitizeHtml(content)
// 安全设置HTML
SafeDOM.setHTML(container, cleanContent)
}
// 安全的URL处理
function handleUserUrl(url) {
if (xssProtection.isValidUrl(url)) {
return SafeDOM.createLink(url, url, { target: '_blank' })
} else {
return SafeDOM.createElement('span', { class: 'invalid-url' }, 'Invalid URL')
}
}
// 导出工具类
window.XSSProtection = XSSProtection
window.InputValidator = InputValidator
window.SafeDOM = SafeDOM
CSRF攻击防护
CSRF令牌机制
// CSRF防护管理器
class CSRFProtection {
constructor(options = {}) {
this.tokenName = options.tokenName || 'csrf_token'
this.headerName = options.headerName || 'X-CSRF-Token'
this.cookieName = options.cookieName || 'csrf_token'
this.tokenLength = options.tokenLength || 32
this.tokenExpiry = options.tokenExpiry || 3600000 // 1小时
this.tokens = new Map()
this.init()
}
init() {
// 生成初始令牌
this.generateToken()
// 设置自动刷新
this.setupTokenRefresh()
// 拦截所有表单提交
this.interceptForms()
// 拦截AJAX请求
this.interceptAjaxRequests()
}
// 生成CSRF令牌
generateToken() {
const token = this.createRandomToken()
const timestamp = Date.now()
this.tokens.set(token, timestamp)
// 设置到cookie
this.setCookie(this.cookieName, token)
// 设置到meta标签
this.setMetaToken(token)
// 清理过期令牌
this.cleanExpiredTokens()
return token
}
// 创建随机令牌
createRandomToken() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
let token = ''
for (let i = 0; i < this.tokenLength; i++) {
token += chars.charAt(Math.floor(Math.random() * chars.length))
}
return token
}
// 验证令牌
validateToken(token) {
if (!token) return false
const timestamp = this.tokens.get(token)
if (!timestamp) return false
// 检查是否过期
const now = Date.now()
if (now - timestamp > this.tokenExpiry) {
this.tokens.delete(token)
return false
}
return true
}
// 获取当前令牌
getCurrentToken() {
// 从meta标签获取
const metaToken = document.querySelector(`meta[name="${this.tokenName}"]`)
if (metaToken) {
return metaToken.getAttribute('content')
}
// 从cookie获取
return this.getCookie(this.cookieName)
}
// 设置Cookie
setCookie(name, value, options = {}) {
const {
expires = new Date(Date.now() + this.tokenExpiry),
path = '/',
secure = location.protocol === 'https:',
sameSite = 'Strict'
} = options
let cookieString = `${name}=${value}; path=${path}; SameSite=${sameSite}`
if (expires) {
cookieString += `; expires=${expires.toUTCString()}`
}
if (secure) {
cookieString += '; Secure'
}
document.cookie = cookieString
}
// 获取Cookie
getCookie(name) {
const value = `; ${document.cookie}`
const parts = value.split(`; ${name}=`)
if (parts.length === 2) {
return parts.pop().split(';').shift()
}
return null
}
// 设置Meta令牌
setMetaToken(token) {
let metaTag = document.querySelector(`meta[name="${this.tokenName}"]`)
if (!metaTag) {
metaTag = document.createElement('meta')
metaTag.name = this.tokenName
document.head.appendChild(metaTag)
}
metaTag.content = token
}
// 清理过期令牌
cleanExpiredTokens() {
const now = Date.now()
for (const [token, timestamp] of this.tokens.entries()) {
if (now - timestamp > this.tokenExpiry) {
this.tokens.delete(token)
}
}
}
// 设置令牌刷新
setupTokenRefresh() {
// 每30分钟刷新一次令牌
setInterval(() => {
this.generateToken()
}, 30 * 60 * 1000)
// 页面可见性变化时刷新令牌
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
this.generateToken()
}
})
}
// 拦截表单提交
interceptForms() {
document.addEventListener('submit', (e) => {
const form = e.target
if (form.tagName !== 'FORM') return
// 跳过GET请求
const method = (form.method || 'GET').toUpperCase()
if (method === 'GET') return
// 检查是否已有CSRF令牌
let tokenInput = form.querySelector(`input[name="${this.tokenName}"]`)
if (!tokenInput) {
tokenInput = document.createElement('input')
tokenInput.type = 'hidden'
tokenInput.name = this.tokenName
form.appendChild(tokenInput)
}
// 设置当前令牌
tokenInput.value = this.getCurrentToken()
})
}
// 拦截AJAX请求
interceptAjaxRequests() {
// 拦截fetch请求
const originalFetch = window.fetch
window.fetch = (url, options = {}) => {
const method = (options.method || 'GET').toUpperCase()
// 只处理非GET请求
if (method !== 'GET') {
options.headers = options.headers || {}
options.headers[this.headerName] = this.getCurrentToken()
}
return originalFetch(url, options)
}
// 拦截XMLHttpRequest
const originalOpen = XMLHttpRequest.prototype.open
const originalSend = XMLHttpRequest.prototype.send
XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
this._method = method.toUpperCase()
return originalOpen.call(this, method, url, async, user, password)
}
XMLHttpRequest.prototype.send = function(data) {
if (this._method && this._method !== 'GET') {
this.setRequestHeader(csrfProtection.headerName, csrfProtection.getCurrentToken())
}
return originalSend.call(this, data)
}
}
// 验证请求来源
validateOrigin(request) {
const origin = request.headers.get('Origin')
const referer = request.headers.get('Referer')
const allowedOrigins = [
location.origin,
// 添加其他允许的源
]
// 检查Origin头
if (origin && !allowedOrigins.includes(origin)) {
return false
}
// 检查Referer头
if (referer) {
const refererUrl = new URL(referer)
if (!allowedOrigins.includes(refererUrl.origin)) {
return false
}
}
return true
}
// 双重提交Cookie验证
validateDoubleSubmitCookie(cookieToken, headerToken) {
return cookieToken && headerToken && cookieToken === headerToken
}
}
// SameSite Cookie管理器
class SameSiteCookieManager {
constructor() {
this.defaultOptions = {
sameSite: 'Strict',
secure: location.protocol === 'https:',
httpOnly: false // 前端需要访问时设为false
}
}
// 设置安全Cookie
setSecureCookie(name, value, options = {}) {
const cookieOptions = {
...this.defaultOptions,
...options
}
let cookieString = `${name}=${value}`
if (cookieOptions.expires) {
cookieString += `; expires=${cookieOptions.expires.toUTCString()}`
}
if (cookieOptions.maxAge) {
cookieString += `; max-age=${cookieOptions.maxAge}`
}
if (cookieOptions.path) {
cookieString += `; path=${cookieOptions.path}`
}
if (cookieOptions.domain) {
cookieString += `; domain=${cookieOptions.domain}`
}
if (cookieOptions.secure) {
cookieString += '; Secure'
}
if (cookieOptions.httpOnly) {
cookieString += '; HttpOnly'
}
if (cookieOptions.sameSite) {
cookieString += `; SameSite=${cookieOptions.sameSite}`
}
document.cookie = cookieString
}
// 删除Cookie
deleteCookie(name, path = '/') {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=${path}`
}
// 获取Cookie
getCookie(name) {
const value = `; ${document.cookie}`
const parts = value.split(`; ${name}=`)
if (parts.length === 2) {
return parts.pop().split(';').shift()
}
return null
}
}
// 请求验证器
class RequestValidator {
constructor() {
this.trustedDomains = new Set([
location.hostname,
// 添加其他信任的域名
])
}
// 验证请求URL
validateRequestUrl(url) {
try {
const urlObj = new URL(url, location.origin)
// 检查协议
if (!['http:', 'https:'].includes(urlObj.protocol)) {
return false
}
// 检查域名
if (!this.trustedDomains.has(urlObj.hostname)) {
return false
}
return true
} catch {
return false
}
}
// 验证表单数据
validateFormData(formData) {
const maxFieldLength = 10000
const maxFieldCount = 100
let fieldCount = 0
for (const [key, value] of formData.entries()) {
fieldCount++
// 检查字段数量
if (fieldCount > maxFieldCount) {
return false
}
// 检查字段长度
if (typeof value === 'string' && value.length > maxFieldLength) {
return false
}
// 检查字段名
if (!/^[a-zA-Z0-9_\-\[\]]+$/.test(key)) {
return false
}
}
return true
}
// 验证JSON数据
validateJsonData(data) {
try {
const jsonString = JSON.stringify(data)
// 检查JSON大小
if (jsonString.length > 1000000) { // 1MB
return false
}
// 检查嵌套深度
if (this.getObjectDepth(data) > 10) {
return false
}
return true
} catch {
return false
}
}
// 获取对象深度
getObjectDepth(obj, depth = 0) {
if (depth > 10) return depth // 防止无限递归
if (typeof obj !== 'object' || obj === null) {
return depth
}
let maxDepth = depth
for (const value of Object.values(obj)) {
const currentDepth = this.getObjectDepth(value, depth + 1)
maxDepth = Math.max(maxDepth, currentDepth)
}
return maxDepth
}
}
// 初始化CSRF防护
const csrfProtection = new CSRFProtection({
tokenName: 'csrf_token',
headerName: 'X-CSRF-Token',
tokenLength: 32,
tokenExpiry: 3600000 // 1小时
})
const cookieManager = new SameSiteCookieManager()
const requestValidator = new RequestValidator()
// 安全的AJAX请求封装
class SecureAjax {
static async request(url, options = {}) {
// 验证URL
if (!requestValidator.validateRequestUrl(url)) {
throw new Error('Invalid request URL')
}
// 设置默认选项
const defaultOptions = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin'
}
const mergedOptions = { ...defaultOptions, ...options }
// 添加CSRF令牌
if (mergedOptions.method !== 'GET') {
mergedOptions.headers[csrfProtection.headerName] = csrfProtection.getCurrentToken()
}
// 验证请求数据
if (mergedOptions.body) {
if (typeof mergedOptions.body === 'string') {
try {
const data = JSON.parse(mergedOptions.body)
if (!requestValidator.validateJsonData(data)) {
throw new Error('Invalid request data')
}
} catch {
// 非JSON数据,跳过验证
}
}
}
try {
const response = await fetch(url, mergedOptions)
// 检查响应状态
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return response
} catch (error) {
console.error('Secure AJAX request failed:', error)
throw error
}
}
static async get(url, options = {}) {
return this.request(url, { ...options, method: 'GET' })
}
static async post(url, data, options = {}) {
return this.request(url, {
...options,
method: 'POST',
body: JSON.stringify(data)
})
}
static async put(url, data, options = {}) {
return this.request(url, {
...options,
method: 'PUT',
body: JSON.stringify(data)
})
}
static async delete(url, options = {}) {
return this.request(url, { ...options, method: 'DELETE' })
}
}
// 使用示例
async function submitForm(formData) {
try {
const response = await SecureAjax.post('/api/submit', formData)
const result = await response.json()
console.log('Form submitted successfully:', result)
} catch (error) {
console.error('Form submission failed:', error)
}
}
// 导出安全工具
window.CSRFProtection = CSRFProtection
window.SameSiteCookieManager = SameSiteCookieManager
window.RequestValidator = RequestValidator
window.SecureAjax = SecureAjax
内容安全策略(CSP)
CSP策略配置
// CSP策略管理器
class CSPManager {
constructor() {
this.policies = new Map()
this.violations = []
this.reportEndpoint = '/api/csp-report'
this.init()
}
init() {
// 设置违规报告监听
this.setupViolationReporting()
// 动态设置CSP策略
this.setDefaultPolicy()
}
// 设置默认CSP策略
setDefaultPolicy() {
const policy = {
'default-src': ["'self'"],
'script-src': [
"'self'",
"'unsafe-inline'", // 仅在必要时使用
'https://cdn.jsdelivr.net',
'https://unpkg.com'
],
'style-src': [
"'self'",
"'unsafe-inline'",
'https://fonts.googleapis.com'
],
'font-src': [
"'self'",
'https://fonts.gstatic.com'
],
'img-src': [
"'self'",
'data:',
'https:'
],
'connect-src': [
"'self'",
'https://api.example.com'
],
'media-src': ["'self'"],
'object-src': ["'none'"],
'frame-src': ["'none'"],
'frame-ancestors': ["'none'"],
'base-uri': ["'self'"],
'form-action': ["'self'"],
'upgrade-insecure-requests': true,
'block-all-mixed-content': true
}
this.setPolicy('default', policy)
}
// 设置CSP策略
setPolicy(name, policy) {
this.policies.set(name, policy)
// 生成CSP头部值
const headerValue = this.generateCSPHeader(policy)
// 设置meta标签
this.setCSPMetaTag(headerValue)
console.log(`CSP policy "${name}" set:`, headerValue)
}
// 生成CSP头部值
generateCSPHeader(policy) {
const directives = []
for (const [directive, values] of Object.entries(policy)) {
if (typeof values === 'boolean' && values) {
directives.push(directive)
} else if (Array.isArray(values)) {
directives.push(`${directive} ${values.join(' ')}`)
}
}
return directives.join('; ')
}
// 设置CSP meta标签
setCSPMetaTag(content) {
let metaTag = document.querySelector('meta[http-equiv="Content-Security-Policy"]')
if (!metaTag) {
metaTag = document.createElement('meta')
metaTag.setAttribute('http-equiv', 'Content-Security-Policy')
document.head.appendChild(metaTag)
}
metaTag.setAttribute('content', content)
}
// 添加源到指令
addSource(directive, source) {
const policy = this.policies.get('default')
if (!policy) return
if (!policy[directive]) {
policy[directive] = []
}
if (!policy[directive].includes(source)) {
policy[directive].push(source)
this.setPolicy('default', policy)
}
}
// 移除源
removeSource(directive, source) {
const policy = this.policies.get('default')
if (!policy || !policy[directive]) return
const index = policy[directive].indexOf(source)
if (index > -1) {
policy[directive].splice(index, 1)
this.setPolicy('default', policy)
}
}
// 设置违规报告
setupViolationReporting() {
document.addEventListener('securitypolicyviolation', (e) => {
const violation = {
blockedURI: e.blockedURI,
columnNumber: e.columnNumber,
disposition: e.disposition,
documentURI: e.documentURI,
effectiveDirective: e.effectiveDirective,
lineNumber: e.lineNumber,
originalPolicy: e.originalPolicy,
referrer: e.referrer,
sample: e.sample,
sourceFile: e.sourceFile,
statusCode: e.statusCode,
violatedDirective: e.violatedDirective,
timestamp: new Date().toISOString()
}
this.handleViolation(violation)
})
}
// 处理CSP违规
handleViolation(violation) {
this.violations.push(violation)
console.warn('CSP Violation:', violation)
// 发送违规报告到服务器
this.reportViolation(violation)
// 限制违规记录数量
if (this.violations.length > 1000) {
this.violations.shift()
}
}
// 报告违规
async reportViolation(violation) {
try {
await fetch(this.reportEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
'csp-report': violation
})
})
} catch (error) {
console.error('Failed to report CSP violation:', error)
}
}
// 获取违规统计
getViolationStats() {
const stats = {}
this.violations.forEach(violation => {
const directive = violation.effectiveDirective
stats[directive] = (stats[directive] || 0) + 1
})
return stats
}
// 生成nonce
generateNonce() {
const array = new Uint8Array(16)
crypto.getRandomValues(array)
return btoa(String.fromCharCode.apply(null, array))
}
// 安全地添加内联脚本
addInlineScript(scriptContent, nonce) {
const script = document.createElement('script')
if (nonce) {
script.nonce = nonce
// 更新CSP策略以包含nonce
this.addSource('script-src', `'nonce-${nonce}'`)
}
script.textContent = scriptContent
document.head.appendChild(script)
return script
}
// 安全地添加内联样式
addInlineStyle(styleContent, nonce) {
const style = document.createElement('style')
if (nonce) {
style.nonce = nonce
// 更新CSP策略以包含nonce
this.addSource('style-src', `'nonce-${nonce}'`)
}
style.textContent = styleContent
document.head.appendChild(style)
return style
}
// 验证外部资源
validateExternalResource(url, type) {
const policy = this.policies.get('default')
if (!policy) return false
const directive = this.getDirectiveForResourceType(type)
const allowedSources = policy[directive] || policy['default-src'] || []
return this.isSourceAllowed(url, allowedSources)
}
// 获取资源类型对应的指令
getDirectiveForResourceType(type) {
const typeMap = {
'script': 'script-src',
'style': 'style-src',
'image': 'img-src',
'font': 'font-src',
'media': 'media-src',
'object': 'object-src',
'frame': 'frame-src'
}
return typeMap[type] || 'default-src'
}
// 检查源是否被允许
isSourceAllowed(url, allowedSources) {
try {
const urlObj = new URL(url)
return allowedSources.some(source => {
if (source === "'self'") {
return urlObj.origin === location.origin
}
if (source === "'none'") {
return false
}
if (source === 'data:') {
return urlObj.protocol === 'data:'
}
if (source === 'https:') {
return urlObj.protocol === 'https:'
}
if (source.startsWith('https://')) {
return url.startsWith(source)
}
// 通配符匹配
if (source.includes('*')) {
const pattern = source.replace(/\*/g, '.*')
const regex = new RegExp(`^${pattern}$`)
return regex.test(url)
}
return url.startsWith(source)
})
} catch {
return false
}
}
// 创建安全的iframe
createSecureIframe(src, options = {}) {
if (!this.validateExternalResource(src, 'frame')) {
console.error('Iframe source not allowed by CSP:', src)
return null
}
const iframe = document.createElement('iframe')
iframe.src = src
// 设置安全属性
iframe.sandbox = options.sandbox || 'allow-scripts allow-same-origin'
iframe.loading = 'lazy'
// 设置其他属性
Object.entries(options).forEach(([key, value]) => {
if (key !== 'sandbox') {
iframe.setAttribute(key, value)
}
})
return iframe
}
}
// 安全资源加载器
class SecureResourceLoader {
constructor(cspManager) {
this.cspManager = cspManager
}
// 安全加载脚本
async loadScript(src, options = {}) {
if (!this.cspManager.validateExternalResource(src, 'script')) {
throw new Error(`Script source not allowed by CSP: ${src}`)
}
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = src
script.async = options.async !== false
script.defer = options.defer || false
script.onload = () => resolve(script)
script.onerror = () => reject(new Error(`Failed to load script: ${src}`))
document.head.appendChild(script)
})
}
// 安全加载样式
async loadStylesheet(href, options = {}) {
if (!this.cspManager.validateExternalResource(href, 'style')) {
throw new Error(`Stylesheet source not allowed by CSP: ${href}`)
}
return new Promise((resolve, reject) => {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = href
link.onload = () => resolve(link)
link.onerror = () => reject(new Error(`Failed to load stylesheet: ${href}`))
document.head.appendChild(link)
})
}
// 安全加载图片
async loadImage(src, options = {}) {
if (!this.cspManager.validateExternalResource(src, 'image')) {
throw new Error(`Image source not allowed by CSP: ${src}`)
}
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => resolve(img)
img.onerror = () => reject(new Error(`Failed to load image: ${src}`))
// 设置属性
Object.entries(options).forEach(([key, value]) => {
img[key] = value
})
img.src = src
})
}
}
// 初始化CSP管理器
const cspManager = new CSPManager()
const resourceLoader = new SecureResourceLoader(cspManager)
// 使用示例
async function loadExternalResources() {
try {
// 安全加载脚本
await resourceLoader.loadScript('https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js')
// 安全加载样式
await resourceLoader.loadStylesheet('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap')
// 安全加载图片
const img = await resourceLoader.loadImage('https://example.com/image.jpg', {
alt: 'Example image',
loading: 'lazy'
})
document.body.appendChild(img)
} catch (error) {
console.error('Failed to load external resources:', error)
}
}
// 导出CSP工具
window.CSPManager = CSPManager
window.SecureResourceLoader = SecureResourceLoader
总结
Web安全最佳实践的核心要点:
- XSS防护:输入验证、输出编码、CSP策略
- CSRF防护:令牌验证、SameSite Cookie、来源检查
- CSP策略:内容安全策略、资源白名单、违规报告
- 安全编码:输入验证、安全DOM操作、错误处理
- 持续监控:安全日志、违规报告、定期审计
Web安全是一个持续的过程,需要在开发的每个阶段都考虑安全因素。通过实施这些最佳实践,可以显著提高Web应用的安全性。