有这样一个场景,首先构建一个docx文件并插入超链接(恶意的链接),上传到文件服务器后获取对应的文件filekey。现在我们提供一个预览接口,通过filekey便可以预览,在根据filekey转html文档返回给页面的时候由于插入的超链接成功触发XSS攻击;
初始预览接口代码示例如下:
@GetMapping(value = "/wordConvertHtml/{fileKey}")
@ApiImplicitParam(name = "fileKey", value = "文件唯一key", required = true)
public void wordConvertHtml(@PathVariable String fileKey, HttpServletResponse response) throws IOException {
OutputStream bos = null;
try {
InputStreamVO inputStreamVO = commonFileService.getInputStreamWithAuthCheck(fileKey);
Document doc = new Document(inputStreamVO.getInputStream());
HtmlSaveOptions opts = new HtmlSaveOptions(SaveFormat.HTML);
opts.setExportXhtmlTransitional(true);
opts.setExportImagesAsBase64(true);
opts.setExportPageSetup(true);
opts.setPrettyFormat(true);
}catch (FileCorruptedException e) {
log.error("word转换HTML发生特定异常:{}", e);
// 给前端提示特殊信息
PrintWriter out = new PrintWriter(bos);
out.println("文件含有特殊格式,系统不兼容。");
out.flush();
out.close();
} catch (Exception e) {
log.error("word转换HTML异常", e);
// 给前端提示信息
response.setContentType("text/html; charset=UTF-8");
PrintWriter out = new PrintWriter(bos);
out.println(e.getMessage());
out.flush();
out.close();
} finally {
if (bos != null) {
bos.close();
}
}
}
处理方式:
添加更全面的安全头设置:确保所有可能返回HTML内容的路径都设置了适当的安全头
1.Content-Security-Policy (CSP)
response.setHeader("Content-Security-Policy", "default-src 'none'; script-src 'none'; style-src 'unsafe-inline'; img-src data:;");
这是一个重要的安全特性,用于防止跨站脚本攻击(XSS)、点击劫持等攻击:
- default-src 'none': 默认情况下不允许从任何源加载资源
- script-src 'none': 不允许执行任何JavaScript代码
- style-src 'unsafe-inline': 允许内联样式(如<style>标签和style属性)
- img-src data:: 只允许通过data URI加载图片
这种配置可以有效防止恶意脚本注入,同时允许内联样式和内嵌图片显示。
2.X-Content-Type-Options
response.setHeader("X-Content-Type-Options", "nosniff");
防止浏览器尝试猜测或"嗅探"响应的内容类型:
- 浏览器会严格按照服务器提供的Content-Type头来处理内容
- 防止某些类型的攻击,比如将JavaScript文件当作图片显示
3.X-Frame-Options
response.setHeader("X-Frame-Options", "DENY");
防止页面被嵌入到iframe中,用于防止点击劫持攻击:
- DENY: 页面不能被嵌入到任何iframe中
- 这可以防止恶意网站将你的页面嵌入到他们的网站中进行钓鱼攻击
4.X-XSS-Protection
response.setHeader("X-XSS-Protection", "1; mode=block");
启用浏览器的内置XSS过滤器:
- 1: 启用XSS过滤
- mode=block: 当检测到XSS攻击时,浏览器会阻止整个页面加载,而不是仅仅过滤掉可疑内容
最后统一对错误进行处理,这样安全等级会更高。
@GetMapping(value = "/wordConvertHtml/{fileKey}")
@ApiImplicitParam(name = "fileKey", value = "文件唯一key", required = true)
public void wordConvertHtml(@PathVariable String fileKey, HttpServletResponse response) throws IOException {
OutputStream bos = null;
try {
bos = response.getOutputStream();
response.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline';");
response.setHeader("X-Content-Type-Options", "nosniff");
response.setHeader("X-Frame-Options", "DENY");
response.setHeader("X-XSS-Protection", "1; mode=block");
InputStreamVO inputStreamVO = commonFileService.getInputStreamWithAuthCheck(fileKey);
Document doc = new Document(inputStreamVO.getInputStream());
HtmlSaveOptions opts = new HtmlSaveOptions(SaveFormat.HTML);
opts.setExportXhtmlTransitional(true);
opts.setExportImagesAsBase64(true);
opts.setExportPageSetup(true);
opts.setPrettyFormat(true);
doc.save(bos, opts);
} catch (FileCorruptedException e) {
log.error("word转换HTML发生特定异常:{}", e);
// 给前端提示特殊信息
// 设置安全响应头
response.setContentType("text/html; charset=UTF-8");
response.setHeader("Content-Security-Policy", "default-src 'none'; script-src 'none';");
response.setHeader("X-Content-Type-Options", "nosniff");
response.setHeader("X-Frame-Options", "DENY");
PrintWriter out = new PrintWriter(bos);
out.println("文件含有特殊格式,系统不兼容。");
out.flush();
out.close();
} catch (Exception e) {
log.error("word转换HTML异常", e);
// 给前端提示信息
// 设置安全响应头
response.setContentType("text/html; charset=UTF-8");
response.setHeader("Content-Security-Policy", "default-src 'none'; script-src 'none';");
response.setHeader("X-Content-Type-Options", "nosniff");
response.setHeader("X-Frame-Options", "DENY");
PrintWriter out = new PrintWriter(bos);
out.println(HtmlUtils.htmlEscape(e.getMessage()));
out.flush();
out.close();
} finally {
if (bos != null) {
bos.close();
}
}
}