前言
在软件开发过程中,确保系统的安全性是至关重要的一环。它不仅关乎保护用户数据的完整性和隐私性,也是维护系统稳定运行的基石。我认为,从宏观角度审视,软件开发的安全性保障主要可分为两大类:功能性的安全性保障和系统性的安全性校验。
功能性的安全性保障专注于应用程序层面,它着眼于那些直接影响用户数据和交互过程安全的特性。这些特性是构建用户信任和保障数据安全的关键。
而系统性的安全性校验则放眼于更为广阔的视角,它涵盖了整个系统架构和网络层面,确保从服务器到网络的每一环节都具备足够的防御能力,以抵御各种潜在的攻击。
在之前 功能性的安全性保障:实现强制登录和密码加密功能 和 功能性的安全性保障:TOKEN鉴权校验 两文里。我们分别讨论了功能性的安全性保障中的几个核心议题:身份验证和密码加密、TOKEN鉴权校验和认证。在本文中,我们依旧继续功能性的安全性保障这个话题,通过对概念的细致解读、实施策略的全面分析,以及实际代码实现的展示,深入讨论其中的另外一个核心议题:输入校验。
1. 怎么控制输入校验
输入校验,一般是对用户输入进行验证,比说文本框输入、图片文件上传、地址输入等。防止出现SQL注入、跨站脚本(XSS)等攻击。
2. 什么是XSS?
XSS(Cross-Site Scripting),跨站脚本攻击,是一种常见的网络安全漏洞,主要形式是攻击者将恶意脚本注入到用户浏览的页面中。这种攻击通常发生在当一个网站允许用户输入未经适当处理的数据到页面时。XSS攻击的影响范围较广,常常被用来盗取用户Cookie、会话劫持、访问敏感数据、冒充用户执行操作、传播恶意软件等。
3. 代码实现:简单模拟XSS攻击
3.1 前端代码实现
这里书接上文,将 MePage.vue 的 个人简介输入框改成TXT文件上传组件。模拟简单的恶意的TXT文件上传导致系统被攻击的场景。
<!-- MePage.vue -->
<template>
<div class="me">
<el-form :model="user" label-width="auto" style="max-width: 600px" enctype="multipart/form-data">
<el-form-item label="头像">
<el-upload
class="avatar-uploader"
:action="uploadUrl"
accept="image/*"
:show-file-list="false"
:before-upload="beforeUpload"
:on-success="handleSuccess"
:on-error="handleError"
:on-change="handleFileChange"
>
<img v-if="imageUrl" style="height: 200px; width: 200px" alt="头像" class="avatar" :src="imageUrl"/>
<el-icon class="avatar-uploader-icon" v-else-if="!islook"/>
</el-upload>
</el-form-item>
<el-form-item label="用户名">
<el-input v-model="user.username"/>
</el-form-item>
<el-form-item label="性别">
<el-input v-model="user.sex"/>
</el-form-item>
<el-form-item label="年龄">
<el-input v-model="user.age"/>
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="user.mailbox"/>
</el-form-item>
<el-form-item label="个人简介">
<el-input v-html="user.introduce"/>
<el-upload
:action="uploadTextUrl"
accept=".txt"
:show-file-list="false"
:on-success="handleTextSuccess"
:on-error="handleTextError">
<el-button type="primary" style="margin-top: 10px;">上传TXT文件</el-button>
</el-upload>
</el-form-item>
</el-form>
</div>
</template>
<script>
import axios from 'axios';
import {ElMessage} from 'element-plus';
export default {
name: 'MePage',
data() {
return {
user: {
userId: '',
sex: '',
age: '',
mailbox: '',
username: '',
introduce: '',
headPortrait: ''
},
uploadUrl: 'http://localhost:8081/upload/avatar',
uploadTextUrl: 'http://localhost:8081/upload/introduce',
islook: false,
imgUrl: '../assets/logo.png',
imageUrl: ''
}
},
components: {},
created() {
this.getuser();
},
methods: {
getuser() {
const token = localStorage.getItem('token');
console.log('token:', token);
if (!token) {
ElMessage.error('请先登录');
this.$router.push('/login');
}
axios.get('http://localhost:8081/user/getById?userId=123', {headers: {Authorization: `Bearer ${token}`}}).then(response => {
console.log('服务器返回的数据:', response.data);
this.user = response.data.body;
this.imageUrl = this.user.headPortrait ? `${this.user.headPortrait}` : '@/assets/logo.png';
console.log('图片 URL:', this.imageUrl);
this.islook = true;
}).catch(error => {
console.log(error);
ElMessage.error('获取用户信息失败');
});
},
beforeUpload(file) {
const isImage = file.type.startsWith('image/');
if (!isImage) {
ElMessage.error('只能上传图片文件!');
return false;
}
return isImage;
},
handleSuccess(response) {
console.log('图片 上传成功响应:', response);
if (response.code === 200) {
ElMessage.success('上传成功');
console.log('上传成功');
// 重新调用 getuser 方法以获取最新的头像地址
this.getuser();
} else {
ElMessage.error('上传失败: ' + response.msg);
}
},
handleError(err) {
console.error('图片上传失败:', err);
ElMessage.error('上传失败: ' + (err.message || '未知错误'));
},
handleFileChange(file) {
// 更新显示的图片
const fileURL = file.raw;
this.imageUrl = URL.createObjectURL(fileURL);
},
handleTextSuccess(response) {
console.log('TXT文件上传成功响应:', response);
if (response && response.code === 200) {
// 直接赋值未编码的内容
this.user.introduce = decodeURIComponent(response.body);
console.log('txt文件内容:', this.user.introduce);
ElMessage.success('TXT文件上传成功');
} else {
ElMessage.error('TXT文件上传失败: ' + (response ? response.msg : '未知错误'));
}
},
handleTextError(error) {
ElMessage.error('上传失败: ' + (error.message || '未知错误'));
}
}
}
</script>
<style scoped>
.me {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
height: 200px;
width: 200px
}
.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
</style>
3.2 后端代码实现
后端只需要提供接口,用于将前端上传的TXT文件进行解析,并将内容回传给前端即可。
/**
* 个人简介上传
*/
@PostMapping("/introduce")
public RestResult<String> uploadIntroduce(@RequestParam("file") MultipartFile file, HttpServletRequest request) throws Exception {
if (!file.isEmpty()) {
// 检查文件类型
if (!Objects.equals(file.getContentType(), "text/plain")) {
Map<String, Object> extension = new HashMap<>();
extension.put("error", "上传格式错误,请上传txt文件!");
return RestResult.buildFailure(extension);
}
// 读取文件内容
StringBuilder content = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
}
return RestResult.build(content.toString());
}
throw new Exception("文件上传失败");
}
让我们来看看效果,上传了一个恶意文件,其内容为:<img src="url" onerror="alert('警告,系统正受到Xss攻击!');">
。通过定义一个不存在的图片路径,当系统尝试加载该图片时会触发错误事件,从而执行恶意脚本,弹出异常提示。这种行为模拟了实际生产环境中,不法分子利用系统漏洞上传恶意文件或可执行插件,诱使系统执行他们预设的脚本,以达到不法目的。
4. 防范Xss攻击
防范跨站脚本攻击(XSS)是Web开发中非常重要的一部分。目前市面上有好几种常用的防范措施:
-
对输入验证和过滤。这点可以使用正则表达式或其他验证工具来实现。
-
前端对后端的输出进行编码。防止浏览器将其解析为可执行的脚本。可以使用模板引擎或手动编码。
-
使用安全的文件上传组件,使用经过安全验证的文件上传组件,并对上传的文件进行严格的检查,确保文件类型和内容符合要求。
-
配置内容安全策略(CSP),限制网页可以加载的资源,防止执行未经授权的脚本。
-
在设置 Cookie 时,使用 HttpOnly 和 Secure 标志,防止通过 JavaScript 访问 Cookie。
4.1 代码实现:防范Xss攻击
针对上面的Xss攻击情况,只需要在后端页面在对文件内容解析后,再对内容进行过滤编码即可,这样传到前端的数据,就不会被当成脚本执行。
/**
* 个人简介上传
*/
@PostMapping("/introduce")
public RestResult<String> uploadIntroduce(@RequestParam("file") MultipartFile file, HttpServletRequest request) throws Exception {
if (!file.isEmpty()) {
// 检查文件类型
if (!Objects.equals(file.getContentType(), "text/plain")) {
Map<String, Object> extension = new HashMap<>();
extension.put("error", "上传格式错误,请上传txt文件!");
return RestResult.buildFailure(extension);
}
// 读取文件内容
StringBuilder content = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
}
// 过滤并编码内容,防止恶意脚本被执行
String filteredContent = filterContent(content.toString());
return RestResult.build(filteredContent);
}
throw new Exception("文件上传失败");
}
/**
* 功能描述 : 过滤内容,防止恶意脚本被执行
*
* @param content 文件内容
* @return {@code String }
* @author xhb
* @since 2024/07/29
*/
private String filterContent(String content) {
return content.replaceAll("<script>", "")
.replaceAll("</script>", "")
.replaceAll("<iframe>", "")
.replaceAll("</iframe>", "")
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll("\"", """)
.replaceAll("'", "'")
.replaceAll("/", "/");
}
5. 什么是SQL注入?
SQL注入(SQL Injection)攻击是指攻击者通过在输入字段中注入恶意SQL代码,从而绕过应用程序的身份验证和授权机制,访问或修改数据库中的数据。SQL注入攻击可以导致数据泄露、数据篡改、权限提升等严重后果。
6. 代码实现:简单模拟SQL注入攻击
6.1 后端代码实现
我们将前面的上传接口实现改一改,通过自定义SQL的形式去更新表数据。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.interestplfm.mapper.UserMapper">
<update id="sqlInjectionToUpdateAvatar">
UPDATE user
SET avatar = #{headPortrait}
WHERE userid = ${userid}
</update>
</mapper>
为了模拟SQL注入,我们可以传递一个包含恶意SQL代码的 id 参数。这里定义一个测试类模拟恶意入参。
@SpringBootTest
class InterestplfmApplicationTests {
@Autowired
private UserRepository userRepository;
@Test
void UploadTest() {
String avatar = "new_avatar.png";
String id = "1; DROP TABLE user; --";
userRepository.sqlInjectionToUpdateAvatar(id , avatar);
}
}
在这种情况下,执行的SQL语句就会变成:
导致在更新用户头衔的同时,将其他用户的个人信息删除掉。
这里顺便小提一嘴,如果有小伙伴在执行的时候出现了org.springframework.jdbc.BadSqlGrammarException
,提示SQL语法错误。且使用的是Mybatis-plus。那么可能是由于MyBatis-Plus在处理SQL语句时,默认会进行参数化查询,不允许多条SQL合并形式,从而防止SQL注入。
解决方法的话,可以在配置文件中,datasource配置的url参数中,加上&allowMultiQueries=true
。允许在单个SQL语句中执行多个查询,从而达到我们想要模拟的效果。
7. 防范SQL注入
防范SQL注入同样是Web开发中非常重要的一部分。常见的防范措施有:
-
使用参数化查询。通过将SQL语句和参数分开处理,数据库引擎会自动转义输入参数,防止恶意代码的执行。
-
对用户输入的数据进行严格的验证和过滤,确保输入的内容符合预期的格式和类型。
-
为数据库用户分配最小必要的权限,避免使用具有高权限的用户执行SQL操作。
-
使用ORM(对象关系映射)框架,如 MyBatis-Plus、Hibernate等,这些框架通常内置了防止SQL注入的机制。
7.1 代码实现:防范SQL注入攻击
其实措施也很简单,我们只需要使用参数化查询,而不是直接拼接SQL语句。在MyBatis中,可以使用 #{} 来代替 ${},这样MyBatis会自动处理参数化查询。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.interestplfm.mapper.UserMapper">
<update id="sqlInjectionToUpdateAvatar">
UPDATE user
SET avatar = #{headPortrait}
WHERE userid = #{userid}
</update>
</mapper>
8. 总结
输入校验是确保应用程序安全的第一道防线。通过对用户输入进行严格的验证,可以阻止恶意数据进入系统,从而减少安全漏洞的风险。有效的输入校验策略可以保护数据的完整性,维护系统稳定性,并增强用户对应用的信任。