Search K
Appearance
Appearance
关键词: 安全最佳实践, 输入验证, 输出编码, 安全表单处理, 文件上传安全, 第三方集成, 安全开发, 防护策略
<!-- 全面的输入验证实现 -->
<script>
// 高级输入验证器
class AdvancedInputValidator {
constructor() {
this.initializeValidationRules();
this.setupValidationMethods();
}
// 初始化验证规则
initializeValidationRules() {
this.validationRules = {
// 基本数据类型验证
string: {
minLength: 1,
maxLength: 255,
pattern: null,
allowEmpty: false
},
number: {
min: Number.MIN_SAFE_INTEGER,
max: Number.MAX_SAFE_INTEGER,
isInteger: false
},
email: {
pattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
maxLength: 254
},
url: {
pattern: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/,
allowedProtocols: ['http', 'https']
},
phone: {
pattern: /^1[3456789]\d{9}$/,
format: 'mobile'
},
// 安全相关验证
password: {
minLength: 8,
maxLength: 128,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialChars: true,
forbiddenPatterns: ['password', '123456', 'qwerty']
},
// 业务相关验证
username: {
minLength: 3,
maxLength: 20,
pattern: /^[a-zA-Z0-9_]{3,20}$/,
forbiddenNames: ['admin', 'root', 'system']
}
};
}
// 设置验证方法
setupValidationMethods() {
this.validationMethods = {
required: (value) => value !== null && value !== undefined && value !== '',
length: (value, min, max) => value.length >= min && value.length <= max,
pattern: (value, pattern) => pattern.test(value),
range: (value, min, max) => value >= min && value <= max,
whitelist: (value, allowedValues) => allowedValues.includes(value),
blacklist: (value, forbiddenValues) => !forbiddenValues.includes(value)
};
}
// 验证字符串
validateString(value, fieldName, options = {}) {
const rules = { ...this.validationRules.string, ...options };
const errors = [];
// 检查是否为空
if (!rules.allowEmpty && !this.validationMethods.required(value)) {
errors.push(`${fieldName}不能为空`);
return { valid: false, errors };
}
if (value) {
// 检查长度
if (!this.validationMethods.length(value, rules.minLength, rules.maxLength)) {
errors.push(`${fieldName}长度必须在${rules.minLength}-${rules.maxLength}之间`);
}
// 检查模式
if (rules.pattern && !this.validationMethods.pattern(value, rules.pattern)) {
errors.push(`${fieldName}格式不正确`);
}
// 安全检查
if (this.containsMaliciousContent(value)) {
errors.push(`${fieldName}包含不安全的内容`);
}
}
return {
valid: errors.length === 0,
errors: errors,
sanitized: this.sanitizeString(value)
};
}
// 验证数字
validateNumber(value, fieldName, options = {}) {
const rules = { ...this.validationRules.number, ...options };
const errors = [];
// 类型检查
const numValue = Number(value);
if (isNaN(numValue)) {
errors.push(`${fieldName}必须是有效的数字`);
return { valid: false, errors };
}
// 整数检查
if (rules.isInteger && !Number.isInteger(numValue)) {
errors.push(`${fieldName}必须是整数`);
}
// 范围检查
if (!this.validationMethods.range(numValue, rules.min, rules.max)) {
errors.push(`${fieldName}必须在${rules.min}-${rules.max}之间`);
}
return {
valid: errors.length === 0,
errors: errors,
sanitized: numValue
};
}
// 验证邮箱
validateEmail(value, fieldName, options = {}) {
const rules = { ...this.validationRules.email, ...options };
const errors = [];
if (!this.validationMethods.required(value)) {
errors.push(`${fieldName}不能为空`);
return { valid: false, errors };
}
// 长度检查
if (value.length > rules.maxLength) {
errors.push(`${fieldName}长度不能超过${rules.maxLength}个字符`);
}
// 格式检查
if (!this.validationMethods.pattern(value, rules.pattern)) {
errors.push(`${fieldName}格式不正确`);
}
// 域名检查
if (this.isDisposableEmail(value)) {
errors.push(`${fieldName}不能使用临时邮箱`);
}
return {
valid: errors.length === 0,
errors: errors,
sanitized: value.toLowerCase().trim()
};
}
// 验证密码
validatePassword(value, fieldName, options = {}) {
const rules = { ...this.validationRules.password, ...options };
const errors = [];
if (!this.validationMethods.required(value)) {
errors.push(`${fieldName}不能为空`);
return { valid: false, errors };
}
// 长度检查
if (!this.validationMethods.length(value, rules.minLength, rules.maxLength)) {
errors.push(`${fieldName}长度必须在${rules.minLength}-${rules.maxLength}之间`);
}
// 强度检查
if (rules.requireUppercase && !/[A-Z]/.test(value)) {
errors.push(`${fieldName}必须包含大写字母`);
}
if (rules.requireLowercase && !/[a-z]/.test(value)) {
errors.push(`${fieldName}必须包含小写字母`);
}
if (rules.requireNumbers && !/\d/.test(value)) {
errors.push(`${fieldName}必须包含数字`);
}
if (rules.requireSpecialChars && !/[!@#$%^&*(),.?":{}|<>]/.test(value)) {
errors.push(`${fieldName}必须包含特殊字符`);
}
// 弱密码检查
const lowerValue = value.toLowerCase();
for (const forbidden of rules.forbiddenPatterns) {
if (lowerValue.includes(forbidden)) {
errors.push(`${fieldName}不能包含常见的弱密码模式`);
break;
}
}
return {
valid: errors.length === 0,
errors: errors,
strength: this.calculatePasswordStrength(value)
};
}
// 验证文件
validateFile(file, fieldName, options = {}) {
const defaultOptions = {
maxSize: 10 * 1024 * 1024, // 10MB
allowedTypes: ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'],
allowedExtensions: ['.jpg', '.jpeg', '.png', '.gif', '.pdf'],
checkMagicNumbers: true
};
const rules = { ...defaultOptions, ...options };
const errors = [];
if (!file || !(file instanceof File)) {
errors.push(`${fieldName}必须是有效的文件`);
return { valid: false, errors };
}
// 大小检查
if (file.size > rules.maxSize) {
errors.push(`${fieldName}大小不能超过${this.formatFileSize(rules.maxSize)}`);
}
// 类型检查
if (!rules.allowedTypes.includes(file.type)) {
errors.push(`${fieldName}类型不被允许`);
}
// 扩展名检查
const fileName = file.name.toLowerCase();
const hasValidExtension = rules.allowedExtensions.some(ext => fileName.endsWith(ext));
if (!hasValidExtension) {
errors.push(`${fieldName}扩展名不被允许`);
}
// 文件名安全检查
if (this.hasUnsafeFileName(file.name)) {
errors.push(`${fieldName}文件名包含不安全的字符`);
}
return {
valid: errors.length === 0,
errors: errors,
fileInfo: {
name: file.name,
size: file.size,
type: file.type,
lastModified: file.lastModified
}
};
}
// 检查恶意内容
containsMaliciousContent(value) {
const maliciousPatterns = [
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
/javascript:/gi,
/vbscript:/gi,
/onload\s*=/gi,
/onerror\s*=/gi,
/onclick\s*=/gi,
/onfocus\s*=/gi,
/onblur\s*=/gi,
/onchange\s*=/gi,
/onsubmit\s*=/gi
];
return maliciousPatterns.some(pattern => pattern.test(value));
}
// 字符串清理
sanitizeString(value) {
if (!value || typeof value !== 'string') {
return '';
}
return value
.trim()
.replace(/\s+/g, ' ') // 合并多个空格
.replace(/[<>]/g, '') // 移除尖括号
.substring(0, 1000); // 限制长度
}
// 检查临时邮箱
isDisposableEmail(email) {
const disposableDomains = [
'10minutemail.com',
'tempmail.org',
'guerrillamail.com',
'mailinator.com',
'throwaway.email'
];
const domain = email.split('@')[1]?.toLowerCase();
return disposableDomains.includes(domain);
}
// 计算密码强度
calculatePasswordStrength(password) {
let score = 0;
// 长度分数
if (password.length >= 8) score += 1;
if (password.length >= 12) score += 1;
if (password.length >= 16) score += 1;
// 字符类型分数
if (/[a-z]/.test(password)) score += 1;
if (/[A-Z]/.test(password)) score += 1;
if (/\d/.test(password)) score += 1;
if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) score += 1;
// 复杂度分数
if (!/(.)\1{2,}/.test(password)) score += 1; // 无重复字符
if (!/012|123|234|345|456|567|678|789|890/.test(password)) score += 1; // 无连续数字
const strength = ['很弱', '弱', '一般', '强', '很强'];
return {
score: score,
level: strength[Math.min(Math.floor(score / 2), 4)]
};
}
// 格式化文件大小
formatFileSize(bytes) {
const sizes = ['B', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 B';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
// 检查不安全的文件名
hasUnsafeFileName(fileName) {
const unsafePatterns = [
/\.\./g, // 路径遍历
/[<>:"|?*]/g, // 不安全字符
/^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i, // Windows保留名
/^\./g, // 隐藏文件
/\s$/g // 末尾空格
];
return unsafePatterns.some(pattern => pattern.test(fileName));
}
}
// 使用示例
const validator = new AdvancedInputValidator();
// 验证表单数据
function validateForm(formData) {
const results = {};
// 验证用户名
results.username = validator.validateString(
formData.username,
'用户名',
{ minLength: 3, maxLength: 20, pattern: /^[a-zA-Z0-9_]+$/ }
);
// 验证邮箱
results.email = validator.validateEmail(formData.email, '邮箱');
// 验证密码
results.password = validator.validatePassword(formData.password, '密码');
// 验证年龄
results.age = validator.validateNumber(
formData.age,
'年龄',
{ min: 1, max: 150, isInteger: true }
);
// 验证文件
if (formData.avatar) {
results.avatar = validator.validateFile(
formData.avatar,
'头像',
{ maxSize: 5 * 1024 * 1024, allowedTypes: ['image/jpeg', 'image/png'] }
);
}
return results;
}
</script><!-- 安全的HTML输出编码 -->
<script>
// HTML编码器
class HTMLEncoder {
constructor() {
this.entityMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
'`': '`',
'=': '='
};
this.contextEncoders = {
html: this.encodeHTML.bind(this),
attribute: this.encodeAttribute.bind(this),
javascript: this.encodeJavaScript.bind(this),
css: this.encodeCSS.bind(this),
url: this.encodeURL.bind(this)
};
}
// HTML内容编码
encodeHTML(text) {
if (!text || typeof text !== 'string') {
return '';
}
return text.replace(/[&<>"'\/`=]/g, (match) => {
return this.entityMap[match];
});
}
// HTML属性编码
encodeAttribute(text) {
if (!text || typeof text !== 'string') {
return '';
}
// 属性值应该总是被引号包围
return text.replace(/[&<>"'\/`=]/g, (match) => {
return this.entityMap[match];
});
}
// JavaScript编码
encodeJavaScript(text) {
if (!text || typeof text !== 'string') {
return '';
}
return text.replace(/[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, (match) => {
return '\\u' + ('0000' + match.charCodeAt(0).toString(16)).slice(-4);
});
}
// CSS编码
encodeCSS(text) {
if (!text || typeof text !== 'string') {
return '';
}
return text.replace(/[^a-zA-Z0-9]/g, (match) => {
return '\\' + match.charCodeAt(0).toString(16) + ' ';
});
}
// URL编码
encodeURL(text) {
if (!text || typeof text !== 'string') {
return '';
}
return encodeURIComponent(text);
}
// 安全的模板渲染
renderTemplate(template, data, context = 'html') {
const encoder = this.contextEncoders[context];
if (!encoder) {
throw new Error(`不支持的编码上下文: ${context}`);
}
return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
const value = data[key.trim()];
return encoder(value);
});
}
// 安全的DOM操作
safeSetTextContent(element, text) {
if (!element || !element.textContent !== undefined) {
console.warn('无效的DOM元素');
return;
}
element.textContent = text || '';
}
safeSetHTML(element, html) {
if (!element || !element.innerHTML !== undefined) {
console.warn('无效的DOM元素');
return;
}
// 先清理HTML
const cleanHTML = this.sanitizeHTML(html);
element.innerHTML = cleanHTML;
}
// HTML清理
sanitizeHTML(html) {
if (!html || typeof html !== 'string') {
return '';
}
// 创建临时DOM元素
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
// 移除脚本标签
const scripts = tempDiv.querySelectorAll('script');
scripts.forEach(script => script.remove());
// 移除危险属性
const allElements = tempDiv.querySelectorAll('*');
allElements.forEach(element => {
// 移除事件处理程序
const attributes = [...element.attributes];
attributes.forEach(attr => {
if (attr.name.startsWith('on')) {
element.removeAttribute(attr.name);
}
});
// 移除javascript:协议
if (element.href && element.href.startsWith('javascript:')) {
element.href = '#';
}
if (element.src && element.src.startsWith('javascript:')) {
element.removeAttribute('src');
}
});
return tempDiv.innerHTML;
}
// 安全的JSON输出
safeJSONStringify(obj) {
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'string') {
return this.encodeHTML(value);
}
return value;
});
}
}
// 使用示例
const encoder = new HTMLEncoder();
// 不同上下文的编码
const userData = {
name: '<script>alert("XSS")</script>',
email: 'user@example.com',
message: 'Hello "World" & <everyone>!'
};
// HTML内容编码
const htmlContent = encoder.renderTemplate(
'<h1>Hello {{name}}</h1><p>{{message}}</p>',
userData,
'html'
);
// 属性编码
const attributeValue = encoder.renderTemplate(
'<input type="text" value="{{name}}" title="{{message}}">',
userData,
'attribute'
);
// JavaScript编码
const jsCode = encoder.renderTemplate(
'var userName = "{{name}}"; console.log(userName);',
userData,
'javascript'
);
console.log('HTML编码:', htmlContent);
console.log('属性编码:', attributeValue);
console.log('JavaScript编码:', jsCode);
</script><!-- 安全的表单处理 -->
<script>
// 安全表单管理器
class SecureFormManager {
constructor() {
this.forms = new Map();
this.tokenGenerator = new TokenGenerator();
this.rateLimiter = new RateLimiter();
this.init();
}
init() {
// 监听表单提交
document.addEventListener('submit', (event) => {
if (event.target.tagName === 'FORM') {
this.handleFormSubmit(event);
}
});
// 监听表单重置
document.addEventListener('reset', (event) => {
if (event.target.tagName === 'FORM') {
this.handleFormReset(event);
}
});
}
// 注册安全表单
registerForm(formId, options = {}) {
const form = document.getElementById(formId);
if (!form) {
throw new Error(`表单 ${formId} 未找到`);
}
const defaultOptions = {
enableCSRF: true,
enableRateLimit: true,
maxSubmissions: 5,
timeWindow: 60000, // 1分钟
validateOnSubmit: true,
encryptSensitiveData: false
};
const formOptions = { ...defaultOptions, ...options };
// 添加CSRF令牌
if (formOptions.enableCSRF) {
this.addCSRFToken(form);
}
// 添加表单验证
if (formOptions.validateOnSubmit) {
this.addFormValidation(form);
}
// 注册表单
this.forms.set(formId, {
form: form,
options: formOptions,
submissionCount: 0,
lastSubmission: 0
});
return formId;
}
// 添加CSRF令牌
addCSRFToken(form) {
// 移除旧的CSRF令牌
const oldToken = form.querySelector('input[name="csrf_token"]');
if (oldToken) {
oldToken.remove();
}
// 生成新的CSRF令牌
const token = this.tokenGenerator.generateCSRFToken();
const tokenInput = document.createElement('input');
tokenInput.type = 'hidden';
tokenInput.name = 'csrf_token';
tokenInput.value = token;
form.appendChild(tokenInput);
// 存储令牌到sessionStorage
sessionStorage.setItem('csrf_token', token);
}
// 添加表单验证
addFormValidation(form) {
const inputs = form.querySelectorAll('input, textarea, select');
inputs.forEach(input => {
// 添加实时验证
input.addEventListener('blur', (event) => {
this.validateField(event.target);
});
// 添加输入过滤
input.addEventListener('input', (event) => {
this.filterInput(event.target);
});
});
}
// 字段验证
validateField(field) {
const validator = new AdvancedInputValidator();
let result = { valid: true, errors: [] };
switch (field.type) {
case 'email':
result = validator.validateEmail(field.value, field.name);
break;
case 'password':
result = validator.validatePassword(field.value, field.name);
break;
case 'tel':
result = validator.validateString(field.value, field.name, {
pattern: /^1[3456789]\d{9}$/
});
break;
case 'url':
result = validator.validateURL(field.value, field.name);
break;
default:
if (field.required) {
result = validator.validateString(field.value, field.name, {
allowEmpty: false
});
}
}
this.showValidationResult(field, result);
return result.valid;
}
// 显示验证结果
showValidationResult(field, result) {
// 移除旧的错误消息
const oldError = field.parentNode.querySelector('.validation-error');
if (oldError) {
oldError.remove();
}
// 更新字段样式
field.classList.remove('valid', 'invalid');
if (result.valid) {
field.classList.add('valid');
} else {
field.classList.add('invalid');
// 显示错误消息
const errorDiv = document.createElement('div');
errorDiv.className = 'validation-error';
errorDiv.textContent = result.errors.join(', ');
field.parentNode.appendChild(errorDiv);
}
}
// 输入过滤
filterInput(field) {
let value = field.value;
// 基本过滤
switch (field.type) {
case 'email':
// 移除不允许的字符
value = value.replace(/[^a-zA-Z0-9@._-]/g, '');
break;
case 'tel':
// 只允许数字
value = value.replace(/[^\d]/g, '');
break;
case 'url':
// 基本URL字符
value = value.replace(/[^a-zA-Z0-9:\/\.\-_?=&%]/g, '');
break;
default:
// 移除潜在的恶意字符
value = value.replace(/[<>]/g, '');
}
// 长度限制
if (field.maxLength && value.length > field.maxLength) {
value = value.substring(0, field.maxLength);
}
field.value = value;
}
// 处理表单提交
handleFormSubmit(event) {
const form = event.target;
const formId = form.id;
const formData = this.forms.get(formId);
if (!formData) {
// 未注册的表单,阻止提交
event.preventDefault();
console.warn('未注册的表单提交被阻止');
return;
}
// 验证CSRF令牌
if (formData.options.enableCSRF && !this.validateCSRFToken(form)) {
event.preventDefault();
this.showError(form, '安全验证失败,请刷新页面重试');
return;
}
// 速率限制检查
if (formData.options.enableRateLimit && !this.checkRateLimit(formId)) {
event.preventDefault();
this.showError(form, '提交过于频繁,请稍后重试');
return;
}
// 表单验证
if (formData.options.validateOnSubmit && !this.validateForm(form)) {
event.preventDefault();
this.showError(form, '请检查表单输入');
return;
}
// 数据加密
if (formData.options.encryptSensitiveData) {
this.encryptSensitiveFields(form);
}
// 更新提交计数
this.updateSubmissionCount(formId);
// 显示提交状态
this.showSubmissionStatus(form);
}
// 验证CSRF令牌
validateCSRFToken(form) {
const formToken = form.querySelector('input[name="csrf_token"]')?.value;
const sessionToken = sessionStorage.getItem('csrf_token');
return formToken && sessionToken && formToken === sessionToken;
}
// 检查速率限制
checkRateLimit(formId) {
const formData = this.forms.get(formId);
const now = Date.now();
// 检查时间窗口
if (now - formData.lastSubmission < formData.options.timeWindow) {
if (formData.submissionCount >= formData.options.maxSubmissions) {
return false;
}
} else {
// 重置计数
formData.submissionCount = 0;
}
return true;
}
// 验证整个表单
validateForm(form) {
const fields = form.querySelectorAll('input, textarea, select');
let isValid = true;
fields.forEach(field => {
if (!this.validateField(field)) {
isValid = false;
}
});
return isValid;
}
// 加密敏感字段
encryptSensitiveFields(form) {
const sensitiveFields = form.querySelectorAll('input[type="password"], input[data-encrypt="true"]');
sensitiveFields.forEach(field => {
if (field.value) {
// 简单加密(实际应用中应使用更强的加密)
field.value = btoa(field.value);
}
});
}
// 更新提交计数
updateSubmissionCount(formId) {
const formData = this.forms.get(formId);
formData.submissionCount++;
formData.lastSubmission = Date.now();
}
// 显示错误消息
showError(form, message) {
let errorDiv = form.querySelector('.form-error');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'form-error';
form.insertBefore(errorDiv, form.firstChild);
}
errorDiv.textContent = message;
errorDiv.style.display = 'block';
// 3秒后自动隐藏
setTimeout(() => {
errorDiv.style.display = 'none';
}, 3000);
}
// 显示提交状态
showSubmissionStatus(form) {
const submitButton = form.querySelector('button[type="submit"], input[type="submit"]');
if (submitButton) {
submitButton.disabled = true;
submitButton.textContent = '提交中...';
// 模拟提交完成
setTimeout(() => {
submitButton.disabled = false;
submitButton.textContent = '提交';
}, 2000);
}
}
// 处理表单重置
handleFormReset(event) {
const form = event.target;
const formId = form.id;
const formData = this.forms.get(formId);
if (formData) {
// 重新生成CSRF令牌
if (formData.options.enableCSRF) {
this.addCSRFToken(form);
}
// 清除验证错误
const errors = form.querySelectorAll('.validation-error');
errors.forEach(error => error.remove());
// 清除字段样式
const fields = form.querySelectorAll('input, textarea, select');
fields.forEach(field => {
field.classList.remove('valid', 'invalid');
});
}
}
}
// 令牌生成器
class TokenGenerator {
generateCSRFToken() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}
}
// 速率限制器
class RateLimiter {
constructor() {
this.attempts = new Map();
}
isAllowed(identifier, maxAttempts = 5, windowMs = 60000) {
const now = Date.now();
const windowStart = now - windowMs;
if (!this.attempts.has(identifier)) {
this.attempts.set(identifier, []);
}
const userAttempts = this.attempts.get(identifier);
// 移除过期的尝试
const validAttempts = userAttempts.filter(timestamp => timestamp > windowStart);
if (validAttempts.length >= maxAttempts) {
return false;
}
// 记录当前尝试
validAttempts.push(now);
this.attempts.set(identifier, validAttempts);
return true;
}
}
// 使用示例
const formManager = new SecureFormManager();
// 注册表单
formManager.registerForm('loginForm', {
enableCSRF: true,
enableRateLimit: true,
maxSubmissions: 3,
timeWindow: 60000,
validateOnSubmit: true
});
formManager.registerForm('profileForm', {
enableCSRF: true,
encryptSensitiveData: true,
validateOnSubmit: true
});
</script><!-- 安全的文件上传 -->
<script>
// 安全文件上传管理器
class SecureFileUploadManager {
constructor() {
this.allowedTypes = {
image: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
document: ['application/pdf', 'text/plain', 'application/msword'],
archive: ['application/zip', 'application/x-rar-compressed']
};
this.maxFileSizes = {
image: 5 * 1024 * 1024, // 5MB
document: 10 * 1024 * 1024, // 10MB
archive: 50 * 1024 * 1024 // 50MB
};
this.uploadQueue = [];
this.uploadProgress = new Map();
this.init();
}
init() {
// 设置文件拖拽处理
this.setupDragAndDrop();
// 设置文件选择处理
this.setupFileInput();
}
// 设置拖拽上传
setupDragAndDrop() {
const dropZones = document.querySelectorAll('.drop-zone');
dropZones.forEach(zone => {
zone.addEventListener('dragover', (event) => {
event.preventDefault();
zone.classList.add('drag-over');
});
zone.addEventListener('dragleave', (event) => {
event.preventDefault();
zone.classList.remove('drag-over');
});
zone.addEventListener('drop', (event) => {
event.preventDefault();
zone.classList.remove('drag-over');
const files = Array.from(event.dataTransfer.files);
this.handleFiles(files, zone);
});
});
}
// 设置文件输入处理
setupFileInput() {
const fileInputs = document.querySelectorAll('input[type="file"]');
fileInputs.forEach(input => {
input.addEventListener('change', (event) => {
const files = Array.from(event.target.files);
this.handleFiles(files, input);
});
});
}
// 处理文件
handleFiles(files, element) {
const uploadType = element.dataset.uploadType || 'image';
files.forEach(file => {
const validation = this.validateFile(file, uploadType);
if (validation.valid) {
this.queueFile(file, uploadType, element);
} else {
this.showError(element, validation.errors.join(', '));
}
});
// 开始上传队列
this.processUploadQueue();
}
// 验证文件
validateFile(file, uploadType) {
const errors = [];
// 检查文件是否存在
if (!file || !(file instanceof File)) {
errors.push('无效的文件');
return { valid: false, errors };
}
// 检查文件类型
const allowedTypes = this.allowedTypes[uploadType] || this.allowedTypes.image;
if (!allowedTypes.includes(file.type)) {
errors.push('文件类型不被允许');
}
// 检查文件大小
const maxSize = this.maxFileSizes[uploadType] || this.maxFileSizes.image;
if (file.size > maxSize) {
errors.push(`文件大小超过限制 (${this.formatFileSize(maxSize)})`);
}
// 检查文件名
if (this.hasUnsafeFileName(file.name)) {
errors.push('文件名包含不安全的字符');
}
// 检查文件扩展名
if (!this.hasValidExtension(file.name, uploadType)) {
errors.push('文件扩展名不被允许');
}
// 检查文件魔数(如果是图片)
if (uploadType === 'image') {
return this.validateImageFile(file, errors);
}
return {
valid: errors.length === 0,
errors: errors
};
}
// 验证图片文件
async validateImageFile(file, errors) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (event) => {
const arrayBuffer = event.target.result;
const bytes = new Uint8Array(arrayBuffer);
// 检查图片魔数
if (!this.isValidImageMagicNumber(bytes, file.type)) {
errors.push('文件类型与实际内容不符');
}
// 检查图片尺寸
this.checkImageDimensions(file).then((dimensions) => {
if (dimensions.width > 5000 || dimensions.height > 5000) {
errors.push('图片尺寸过大');
}
resolve({
valid: errors.length === 0,
errors: errors,
dimensions: dimensions
});
});
};
reader.onerror = () => {
errors.push('文件读取失败');
resolve({ valid: false, errors });
};
reader.readAsArrayBuffer(file.slice(0, 1024)); // 只读取前1024字节
});
}
// 检查图片魔数
isValidImageMagicNumber(bytes, mimeType) {
const magicNumbers = {
'image/jpeg': [0xFF, 0xD8, 0xFF],
'image/png': [0x89, 0x50, 0x4E, 0x47],
'image/gif': [0x47, 0x49, 0x46, 0x38],
'image/webp': [0x52, 0x49, 0x46, 0x46]
};
const expectedMagic = magicNumbers[mimeType];
if (!expectedMagic) {
return false;
}
for (let i = 0; i < expectedMagic.length; i++) {
if (bytes[i] !== expectedMagic[i]) {
return false;
}
}
return true;
}
// 检查图片尺寸
checkImageDimensions(file) {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
resolve({
width: img.width,
height: img.height
});
};
img.onerror = () => {
resolve({ width: 0, height: 0 });
};
img.src = URL.createObjectURL(file);
});
}
// 检查文件名安全性
hasUnsafeFileName(fileName) {
const unsafePatterns = [
/\.\./g, // 路径遍历
/[<>:"|?*]/g, // 不安全字符
/^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i, // Windows保留名
/^\./g, // 隐藏文件
/\s$/g, // 末尾空格
/\0/g // null字符
];
return unsafePatterns.some(pattern => pattern.test(fileName));
}
// 检查文件扩展名
hasValidExtension(fileName, uploadType) {
const validExtensions = {
image: ['.jpg', '.jpeg', '.png', '.gif', '.webp'],
document: ['.pdf', '.txt', '.doc', '.docx'],
archive: ['.zip', '.rar']
};
const allowed = validExtensions[uploadType] || validExtensions.image;
const extension = fileName.toLowerCase().split('.').pop();
return allowed.includes('.' + extension);
}
// 加入上传队列
queueFile(file, uploadType, element) {
const fileId = this.generateFileId();
const uploadItem = {
id: fileId,
file: file,
type: uploadType,
element: element,
status: 'queued',
progress: 0,
error: null
};
this.uploadQueue.push(uploadItem);
this.uploadProgress.set(fileId, uploadItem);
// 显示上传项
this.showUploadItem(uploadItem);
}
// 生成文件ID
generateFileId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
// 显示上传项
showUploadItem(uploadItem) {
const container = document.getElementById('upload-progress') || this.createUploadContainer();
const itemDiv = document.createElement('div');
itemDiv.className = 'upload-item';
itemDiv.id = `upload-${uploadItem.id}`;
itemDiv.innerHTML = `
<div class="upload-info">
<span class="file-name">${uploadItem.file.name}</span>
<span class="file-size">${this.formatFileSize(uploadItem.file.size)}</span>
</div>
<div class="upload-progress">
<div class="progress-bar">
<div class="progress-fill" style="width: 0%"></div>
</div>
<span class="progress-text">0%</span>
</div>
<div class="upload-actions">
<button class="cancel-btn" onclick="uploadManager.cancelUpload('${uploadItem.id}')">取消</button>
</div>
`;
container.appendChild(itemDiv);
}
// 创建上传容器
createUploadContainer() {
const container = document.createElement('div');
container.id = 'upload-progress';
container.className = 'upload-progress-container';
document.body.appendChild(container);
return container;
}
// 处理上传队列
async processUploadQueue() {
const maxConcurrentUploads = 3;
const uploading = [];
while (this.uploadQueue.length > 0 || uploading.length > 0) {
// 启动新的上传
while (uploading.length < maxConcurrentUploads && this.uploadQueue.length > 0) {
const uploadItem = this.uploadQueue.shift();
const uploadPromise = this.uploadFile(uploadItem);
uploading.push(uploadPromise);
}
// 等待一个上传完成
if (uploading.length > 0) {
await Promise.race(uploading);
// 移除已完成的上传
uploading.splice(0, uploading.length);
}
}
}
// 上传文件
async uploadFile(uploadItem) {
try {
uploadItem.status = 'uploading';
// 创建FormData
const formData = new FormData();
formData.append('file', uploadItem.file);
formData.append('type', uploadItem.type);
formData.append('fileId', uploadItem.id);
// 添加CSRF令牌
const csrfToken = sessionStorage.getItem('csrf_token');
if (csrfToken) {
formData.append('csrf_token', csrfToken);
}
// 上传请求
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
credentials: 'same-origin'
});
if (!response.ok) {
throw new Error(`上传失败: ${response.status}`);
}
const result = await response.json();
uploadItem.status = 'completed';
uploadItem.result = result;
this.updateUploadProgress(uploadItem, 100);
this.showUploadSuccess(uploadItem);
} catch (error) {
uploadItem.status = 'error';
uploadItem.error = error.message;
this.showUploadError(uploadItem);
}
}
// 更新上传进度
updateUploadProgress(uploadItem, progress) {
const itemElement = document.getElementById(`upload-${uploadItem.id}`);
if (itemElement) {
const progressFill = itemElement.querySelector('.progress-fill');
const progressText = itemElement.querySelector('.progress-text');
if (progressFill) {
progressFill.style.width = `${progress}%`;
}
if (progressText) {
progressText.textContent = `${progress}%`;
}
}
}
// 显示上传成功
showUploadSuccess(uploadItem) {
const itemElement = document.getElementById(`upload-${uploadItem.id}`);
if (itemElement) {
itemElement.classList.add('upload-success');
const actionsDiv = itemElement.querySelector('.upload-actions');
if (actionsDiv) {
actionsDiv.innerHTML = '<span class="success-message">上传成功</span>';
}
}
}
// 显示上传错误
showUploadError(uploadItem) {
const itemElement = document.getElementById(`upload-${uploadItem.id}`);
if (itemElement) {
itemElement.classList.add('upload-error');
const actionsDiv = itemElement.querySelector('.upload-actions');
if (actionsDiv) {
actionsDiv.innerHTML = `
<span class="error-message">${uploadItem.error}</span>
<button onclick="uploadManager.retryUpload('${uploadItem.id}')">重试</button>
`;
}
}
}
// 取消上传
cancelUpload(fileId) {
const uploadItem = this.uploadProgress.get(fileId);
if (uploadItem) {
uploadItem.status = 'cancelled';
// 从队列中移除
const queueIndex = this.uploadQueue.findIndex(item => item.id === fileId);
if (queueIndex !== -1) {
this.uploadQueue.splice(queueIndex, 1);
}
// 移除UI元素
const itemElement = document.getElementById(`upload-${fileId}`);
if (itemElement) {
itemElement.remove();
}
this.uploadProgress.delete(fileId);
}
}
// 重试上传
retryUpload(fileId) {
const uploadItem = this.uploadProgress.get(fileId);
if (uploadItem) {
uploadItem.status = 'queued';
uploadItem.error = null;
this.uploadQueue.push(uploadItem);
this.processUploadQueue();
}
}
// 格式化文件大小
formatFileSize(bytes) {
const sizes = ['B', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 B';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
// 显示错误消息
showError(element, message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'upload-error-message';
errorDiv.textContent = message;
element.parentNode.insertBefore(errorDiv, element.nextSibling);
setTimeout(() => {
errorDiv.remove();
}, 5000);
}
}
// 初始化文件上传管理器
const uploadManager = new SecureFileUploadManager();
</script>
<!-- 上传界面HTML -->
<div class="upload-container">
<div class="drop-zone" data-upload-type="image">
<p>拖拽图片文件到此处或点击选择</p>
<input type="file" accept="image/*" multiple>
</div>
<div class="drop-zone" data-upload-type="document">
<p>拖拽文档文件到此处或点击选择</p>
<input type="file" accept=".pdf,.txt,.doc,.docx" multiple>
</div>
</div>
<style>
.upload-container {
max-width: 800px;
margin: 20px auto;
padding: 20px;
}
.drop-zone {
border: 2px dashed #ccc;
border-radius: 8px;
padding: 40px;
text-align: center;
margin-bottom: 20px;
cursor: pointer;
transition: border-color 0.3s;
}
.drop-zone:hover,
.drop-zone.drag-over {
border-color: #007cba;
background-color: #f0f8ff;
}
.upload-progress-container {
margin-top: 20px;
}
.upload-item {
border: 1px solid #ddd;
border-radius: 4px;
padding: 15px;
margin-bottom: 10px;
background: #fff;
}
.upload-item.upload-success {
border-color: #4caf50;
background-color: #f8fff8;
}
.upload-item.upload-error {
border-color: #f44336;
background-color: #fff8f8;
}
.upload-info {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.progress-bar {
width: 100%;
height: 20px;
background-color: #f0f0f0;
border-radius: 10px;
overflow: hidden;
margin-bottom: 10px;
}
.progress-fill {
height: 100%;
background-color: #007cba;
transition: width 0.3s ease;
}
.upload-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.success-message {
color: #4caf50;
font-weight: bold;
}
.error-message {
color: #f44336;
font-weight: bold;
}
.upload-error-message {
color: #f44336;
font-size: 14px;
margin-top: 5px;
padding: 5px;
border-radius: 3px;
background-color: #fff8f8;
}
</style>// 第三方集成安全管理器
class SecureThirdPartyManager {
constructor() {
this.trustedDomains = new Set([
'apis.google.com',
'cdn.jsdelivr.net',
'cdnjs.cloudflare.com',
'unpkg.com'
]);
this.loadedScripts = new Map();
this.apiKeys = new Map();
this.rateLimits = new Map();
this.init();
}
init() {
// 监控脚本加载
this.monitorScriptLoading();
// 设置API密钥管理
this.setupAPIKeyManagement();
// 监控网络请求
this.monitorNetworkRequests();
}
// 安全加载第三方脚本
async loadSecureScript(url, options = {}) {
const defaultOptions = {
integrity: null,
crossorigin: 'anonymous',
timeout: 10000,
retries: 3
};
const mergedOptions = { ...defaultOptions, ...options };
try {
// 验证URL
if (!this.isValidScriptURL(url)) {
throw new Error('不可信的脚本URL');
}
// 检查是否已加载
if (this.loadedScripts.has(url)) {
return this.loadedScripts.get(url);
}
// 加载脚本
const script = await this.loadScript(url, mergedOptions);
this.loadedScripts.set(url, script);
return script;
} catch (error) {
console.error('脚本加载失败:', error);
throw error;
}
}
// 验证脚本URL
isValidScriptURL(url) {
try {
const urlObj = new URL(url);
// 检查协议
if (!['https:', 'http:'].includes(urlObj.protocol)) {
return false;
}
// 检查域名
if (!this.trustedDomains.has(urlObj.hostname)) {
console.warn('不可信的域名:', urlObj.hostname);
return false;
}
// 检查路径
if (urlObj.pathname.includes('..')) {
return false;
}
return true;
} catch (error) {
return false;
}
}
// 加载脚本
loadScript(url, options) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = url;
script.async = true;
// 设置安全属性
if (options.integrity) {
script.integrity = options.integrity;
}
if (options.crossorigin) {
script.crossOrigin = options.crossorigin;
}
// 设置超时
const timeoutId = setTimeout(() => {
script.remove();
reject(new Error('脚本加载超时'));
}, options.timeout);
script.onload = () => {
clearTimeout(timeoutId);
resolve(script);
};
script.onerror = () => {
clearTimeout(timeoutId);
script.remove();
reject(new Error('脚本加载失败'));
};
document.head.appendChild(script);
});
}
// 安全API调用
async secureAPICall(endpoint, options = {}) {
const defaultOptions = {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
timeout: 30000,
retries: 3,
rateLimit: true
};
const mergedOptions = { ...defaultOptions, ...options };
try {
// 验证端点
if (!this.isValidAPIEndpoint(endpoint)) {
throw new Error('不可信的API端点');
}
// 检查速率限制
if (mergedOptions.rateLimit && !this.checkRateLimit(endpoint)) {
throw new Error('API调用过于频繁');
}
// 添加认证
if (mergedOptions.requireAuth) {
this.addAuthentication(mergedOptions, endpoint);
}
// 发送请求
const response = await this.makeRequest(endpoint, mergedOptions);
// 验证响应
this.validateResponse(response);
return response;
} catch (error) {
console.error('API调用失败:', error);
throw error;
}
}
// 验证API端点
isValidAPIEndpoint(endpoint) {
try {
const url = new URL(endpoint);
// 检查协议
if (url.protocol !== 'https:') {
return false;
}
// 检查域名白名单
const trustedAPIs = [
'api.github.com',
'jsonplaceholder.typicode.com',
'httpbin.org'
];
return trustedAPIs.includes(url.hostname);
} catch (error) {
return false;
}
}
// 检查速率限制
checkRateLimit(endpoint) {
const now = Date.now();
const windowMs = 60000; // 1分钟
const maxRequests = 100;
if (!this.rateLimits.has(endpoint)) {
this.rateLimits.set(endpoint, []);
}
const requests = this.rateLimits.get(endpoint);
// 移除过期的请求
const validRequests = requests.filter(timestamp => now - timestamp < windowMs);
if (validRequests.length >= maxRequests) {
return false;
}
// 记录当前请求
validRequests.push(now);
this.rateLimits.set(endpoint, validRequests);
return true;
}
// 添加认证
addAuthentication(options, endpoint) {
const apiKey = this.apiKeys.get(endpoint);
if (apiKey) {
options.headers['Authorization'] = `Bearer ${apiKey}`;
}
}
// 发送请求
async makeRequest(endpoint, options) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), options.timeout);
try {
const response = await fetch(endpoint, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
return response;
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
// 验证响应
validateResponse(response) {
// 检查响应头
const contentType = response.headers.get('Content-Type');
if (!contentType || !contentType.includes('application/json')) {
console.warn('响应内容类型不是JSON');
}
// 检查安全头
const securityHeaders = [
'X-Content-Type-Options',
'X-Frame-Options',
'X-XSS-Protection'
];
securityHeaders.forEach(header => {
if (!response.headers.has(header)) {
console.warn(`缺少安全头: ${header}`);
}
});
}
// 安全设置API密钥
setAPIKey(endpoint, apiKey) {
// 验证API密钥格式
if (!this.isValidAPIKey(apiKey)) {
throw new Error('无效的API密钥格式');
}
this.apiKeys.set(endpoint, apiKey);
// 设置定期轮换提醒
this.scheduleKeyRotation(endpoint);
}
// 验证API密钥
isValidAPIKey(apiKey) {
// 检查长度
if (apiKey.length < 32) {
return false;
}
// 检查格式
if (!/^[a-zA-Z0-9_-]+$/.test(apiKey)) {
return false;
}
return true;
}
// 监控脚本加载
monitorScriptLoading() {
const originalCreateElement = document.createElement;
document.createElement = function(tagName) {
const element = originalCreateElement.call(this, tagName);
if (tagName.toLowerCase() === 'script') {
// 监控脚本属性设置
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
const src = element.getAttribute('src');
if (src && !this.isValidScriptURL(src)) {
console.warn('检测到不可信的脚本加载:', src);
}
}
});
});
observer.observe(element, { attributes: true });
}
return element;
};
}
// 监控网络请求
monitorNetworkRequests() {
const originalFetch = window.fetch;
window.fetch = async function(resource, options = {}) {
const url = typeof resource === 'string' ? resource : resource.url;
// 记录请求
console.log('网络请求:', url);
// 检查请求安全性
if (!this.isSecureRequest(url, options)) {
console.warn('不安全的网络请求:', url);
}
return originalFetch.call(this, resource, options);
};
}
// 检查请求安全性
isSecureRequest(url, options) {
try {
const urlObj = new URL(url);
// 检查协议
if (urlObj.protocol !== 'https:' && urlObj.hostname !== 'localhost') {
return false;
}
// 检查凭据
if (options.credentials === 'include' && urlObj.origin !== window.location.origin) {
console.warn('跨域请求包含凭据');
}
return true;
} catch (error) {
return false;
}
}
// 定期轮换API密钥
scheduleKeyRotation(endpoint) {
const rotationInterval = 30 * 24 * 60 * 60 * 1000; // 30天
setTimeout(() => {
console.warn(`API密钥需要轮换: ${endpoint}`);
// 这里可以触发密钥轮换流程
}, rotationInterval);
}
}
// 使用示例
const thirdPartyManager = new SecureThirdPartyManager();
// 安全加载第三方脚本
async function loadGoogleMaps() {
try {
await thirdPartyManager.loadSecureScript(
'https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY',
{
integrity: 'sha384-...',
crossorigin: 'anonymous'
}
);
// 初始化地图
initializeMap();
} catch (error) {
console.error('Google Maps加载失败:', error);
}
}
// 安全API调用
async function fetchUserData() {
try {
const response = await thirdPartyManager.secureAPICall(
'https://jsonplaceholder.typicode.com/users/1',
{
method: 'GET',
requireAuth: false,
rateLimit: true
}
);
const data = await response.json();
console.log('用户数据:', data);
} catch (error) {
console.error('用户数据获取失败:', error);
}
}A: 客户端验证只能提供用户体验,不能替代服务器端验证。所有安全关键的验证必须在服务器端进行。
A: 根据输出上下文选择:HTML内容用HTML编码,属性值用属性编码,JavaScript中用JavaScript编码。
A: 主要风险包括恶意文件执行、路径遍历、文件类型伪造、存储空间耗尽等。
A: 验证第三方URL、使用HTTPS、设置CSP、限制权限、监控API调用等。
A: CSRF令牌应该是随机生成、唯一的、有时效性的,并且只能通过安全的方式传输。
本章总结:本章详细介绍了HTML5安全性的各个方面,包括Web安全基础、HTML5安全特性和安全最佳实践。通过学习这些内容,您将能够构建更加安全的Web应用程序。