安全开发原则与编码规范
字数 2453 2025-08-20 18:17:02
安全开发原则与编码规范
第一章 前言
软件安全开发生命周期(SDL,Security Development Lifecycle)是一个旨在帮助开发人员构建更安全软件的开发过程,其核心理念是将安全考虑集成在软件开发的每一个阶段:需求分析、设计、编码、测试和维护,以减少漏洞并提高系统安全性。
第二章 安全编码原则
2.1 上线前通用安全开发要求
- 使用经测试和可信的平台/框架代码开发应用程序,并保持对所依赖的框架更新
- log4j等第三方组件应当升级至最高版本
- 避免于页面中包含技术性注释语句、功能说明、个人信息或解释
- 代码上线前应删除测试内容(测试页面、测试代码等)
- 删除默认部署页面,禁止留存SVN/Git相关文件、备份文件
- 客户端应用(如移动APP)应使用混淆、签名、加固等措施
- 禁止使用phpMyAdmin、Struts 1/2、Fastjson组件
- 禁止特殊组件(德鲁伊、Actuator、nacos等)对外网暴露
- 禁止使用应用组件缺省账号和密码
- 禁止swagger等接口文档对外开放
2.2 输入验证
- 必须在后台服务完成数据校验
- 判断输入是否符合预期的数据类型、长度、数据范围
- 采用白名单形式进行输入校验
- 对常见危险字符(<>'"%|;&/\)结合业务场景过滤
- 从框架层面进行全局处理,避免前后处理不一致
- 内部服务传递的数据也应进行校验
2.3 身份验证与会话管理
- 避免在URL中传递会话标识
- 控制用户登录鉴权的Cookie应设置HttpOnly属性
- 实现全站HTTPS后,Cookie应设置secure属性
- 设置会话令牌有效期(建议公网系统不超过30分钟)
- 用户注销时立即清理当前用户会话
- 执行关键操作前应再次验证用户身份
- 禁止userid、roleid等关键身份鉴别参数被外部控制
2.4 注册/登录/忘记密码
2.4.1 自行实现功能安全要点
- 管理平台禁止自行实现注册功能
- 2B业务系统注册需配有审核流程
- 注册接口应使用短信或邮箱验证码
- 限制用户名合法字符和长度;密码需满足复杂度要求
- 登录失败时不应返回详细提示
- 登录验证码每校验过一次应立即失效
- 后台管理页面需记录成功登录用户名和IP、时间
- 非常登录IP应进行多因素二次验证
- 密码输入界面不应以明文显示
- 校验手机号/邮箱和验证码的关联性
2.4.2 敏感/核心业务必须采用登录态隔离
- 敏感业务需采用登录态隔离方案,通过统一登录平台下发业务私有凭证
2.5 访问控制
- 游客身份不应使用预埋账号
- 接口返回数据需遵循最小化原则
- 限制登录尝试频率和次数
- 新注册用户遵循权限最小化原则
- 短信验证码应采用防爆破机制
- 防止参数置空后进行全量数据查询
- 用户个人页面或功能需严格权限控制
- 确保只有授权用户才能访问敏感数据
2.6 传输安全与加密
- 增删改操作使用POST方法提交
- 所有Web页面和HTTP API接口必须通过HTTPS(TLS 1.2+)
- 算法选择:
- 对称加密:AES-128+
- 非对称加密:RSA-2048+、DSA-2048+、ECC-256+
- 哈希算法:SHA-2(SHA-256)或SHA-3
2.7 数据保护
- 不在客户端明文保存敏感信息
- 个人隐私敏感信息需加密存储并脱敏显示
- ID参数不能是数序数字(使用16位以上随机编码)
- 涉及用户隐私、高价值信息的接口需做频率控制
2.8 文件上传、下载
- 建议搭建本地文件服务器
- 文件上传前需验证用户身份
- 以白名单形式校验限制上传文件类型
- 验证文件包头(content-type)信息是否匹配
- 限制解压后的文件数量和总大小
- 文件禁止保存在Web容器内
- 第三方存储服务需检查权限配置
- 禁止直传OSS存储,禁止密钥写入客户端
- 对上传文件进行安全重命名(16位以上)
- 验证下载文件名防止路径遍历
- 不返回文件保存的绝对路径
- 验证文件真实路径是否在允许范围内
- 及时关闭字节/字符流
2.9 支付逻辑
- 支付接口需严格校验入参:
- 禁止用户修改订单金额
- 禁止用户修改支付状态
- 防止优惠券并发领取和重用
- 优惠券id需采用安全随机算法生成(16位以上)
- 判断优惠券归属
- 服务端需对数量和金额设定合理上限
- 防止金额四舍五入
- 禁止商品数量为非整数
- 禁止频繁注销注册重置体验日期
- 禁止并发提现
2.10 异常处理与日志审计
- 精确捕获异常并恰当处理
- 不将系统异常信息直接反馈给用户
- 创建默认错误页面,使用通用错误消息
- 不在日志中保存敏感信息
- 对日志中的特殊元素进行过滤和验证
- 留存相关网络日志不少于六个月
2.11 编码相关
- 及时删除废弃的冗余代码
- 不应存在永真和永假代码
- 禁止将敏感信息写死在代码中
- 禁止使用默认或易猜解的密钥
- 禁止危险的方法或函数public
- 遵守正确的行为次序
- 防止边界操作发生越界
- 防止遗漏边界值检查
- 防止除零错误
第三章 安全编码示例
3.1 命令注入
错误示范:
String command = "ping " + args[0]; // 直接拼接用户输入
Process process = Runtime.getRuntime().exec(command);
修复:
private static String sanitizeInput(String input) {
return input.replaceAll("[;|&<>{}]", "");
}
String command = "ping " + sanitizeInput(args[0]);
3.2 Cookie验证
错误示范:
Cookie[] cookies = request.getCookies();
String userRole = null;
for (Cookie c : cookies) {
if (c.getName().equals("role")) {
userRole = c.getValue(); // 直接从cookie读取角色
}
}
修复:
// 存储
HttpSession session = request.getSession();
session.setAttribute("userId", "12345");
session.setAttribute("role", "admin");
// 获取
String role = (String) session.getAttribute("role");
3.3 内存溢出
错误示范:
public List<ThirdPaySlave> selectByPayNoThird(String payNoThird) {
return thirdPaySlaveMapper.selectByPayNoThird(payNoThird); // 未判空
}
修复:
if (StringUtils.isBlank(payNoThird)) {
return new ArrayList<>(); // 参数判空
}
3.4 整数溢出
错误示范:
int a = 2147483647;
int b = 1;
int sum = a + b; // 溢出
修复:
long a = 2147483647L;
long b = 1;
long sum = a + b;
3.5 除零错误
错误示范:
int a = 10;
int b = 0;
int result = a / b; // 除零异常
修复:
try {
result = a / b;
} catch (ArithmeticException e) {
System.out.println("Error: Division by zero");
}
3.6 边界值检查
错误示范:
if (a < b) {
// ...
} else if (a > b) {
// ... // 遗漏相等情况
}
修复:
if (a < b) {
// ...
} else if (a > b) {
// ...
} else {
// 处理相等情况
}
3.7 随机数生成
错误示范:
Random random = new Random();
for (int i = 0; i < 16; i++) {
sb.append(random.nextInt(2)); // 仅生成0或1
}
修复:
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[16];
random.nextBytes(bytes);
for (byte b : bytes) {
sb.append(Integer.toHexString((b & 0xFF) | 0x100).substring(1,3));
}
3.8 信息泄露
错误示范:
catch (IOException e) {
e.printStackTrace();
System.out.println("数据库密码:" + dbPassword); // 打印敏感信息
}
修复:
catch (IOException e) {
logger.error("账号密码逻辑处理异常", e); // 使用日志记录
}
3.9 并发控制
错误示范:
public void increment() {
count++; // 非线程安全
}
修复:
public synchronized void increment() {
count++;
}
// 或
private static Object lock = new Object();
synchronized (lock) {
count++;
}
3.10 资源释放
错误示范:
FileInputStream fis = new FileInputStream("file.txt");
// 使用后未关闭
修复:
try (FileInputStream fis = new FileInputStream("file.txt")) {
// 使用资源
} // 自动关闭
3.11 危险方法暴露
错误示范:
public void removeDatabase(String name) {
stmt.execute("DROP DATABASE " + name); // 危险方法公开
}
修复:
private void removeDatabase(String name) { // 改为私有
// ...
}
3.12 操作顺序
错误示范:
File file = new File("file.txt");
FileReader reader = new FileReader(file); // 先读取
if (file.canRead()) { // 后检查权限
// ...
}
修复:
if (file.canRead()) { // 先检查权限
FileReader reader = new FileReader(file); // 后读取
// ...
}
3.13 压缩炸弹防护
错误示范:
if (file.length() > MAX_SIZE) {
// ...
} else {
// 直接解压 // 未检查解压后大小
}
修复:
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(file))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (entry.getSize() > MAX_SIZE) {
continue; // 检查每个条目大小
}
// 处理
}
}
3.14 数组越界
错误示范:
int[] arr = new int[5];
for (int i = 0; i < 6; i++) { // 可能越界
arr[i] = i;
}
修复:
for (int i = 0; i < arr.length; i++) { // 使用length属性
arr[i] = i;
}
3.15 会话失效
错误示范:
request.getSession().setMaxInactiveInterval(-1); // 会话永不过期
修复:
request.getSession().setMaxInactiveInterval(30 * 60); // 30分钟过期
3.16 SQL注入
错误示范:
@Select("SELECT * FROM users WHERE username = '${username}'") // 使用${}
User getUser(@Param("username") String username);
修复:
@Select("SELECT * FROM users WHERE username = #{username}") // 使用#{}
User getUser(@Param("username") String username);
3.17 XSS防护
错误示范:
model.addAttribute("username", username); // 直接输出未过滤
修复:
String safeUsername = Jsoup.clean(username, Safelist.basic());
model.addAttribute("username", safeUsername);
全局过滤器示例:
public class XssFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
String safeInput = Jsoup.clean(request.getParameter("input"), Safelist.basic());
request.setAttribute("input", safeInput);
chain.doFilter(request, response);
}
}
3.18 路径遍历
错误示范:
String path = "/safe_dir/" + inputPath;
File f = new File(path); // 可能包含../
f.delete();
修复:
Path path = Paths.get("/safe_dir/", inputPath).normalize(); // 规范化路径
if (path.startsWith("/safe_dir/")) {
Files.delete(path);
}
3.19 文件上传
错误示范:
Part filePart = request.getPart("file");
filePart.write("uploads/" + filePart.getSubmittedFileName()); // 无任何校验
修复:
// 1. 校验文件类型、大小和后缀名
if (!ALLOWED_TYPE.equals(filePart.getContentType())
|| filePart.getSize() > MAX_SIZE
|| !ALLOWED_EXTENSIONS.contains(getExtension(fileName))) {
return;
}
// 2. 重命名文件
String newName = System.currentTimeMillis() + "_" + random.nextInt(1000) + ext;
filePart.write("uploads/" + newName);
3.20 OGNL注入
错误示范:
String expression = request.getParameter("exp"); // 用户可控
Object value = Ognl.getValue(expression, context, root); // 直接执行
修复:
String expression = request.getParameter("exp");
if (hasSpecialChars(expression)) { // 检查特殊字符
return;
}
Object value = Ognl.getValue(expression, context, root);
public boolean hasSpecialChars(String input) {
Pattern p = Pattern.compile("[`~!@#$%^&*()+=|{}':;'\\\
$$
\\\
$$
<>/?]");
return p.matcher(input).find();
}
3.21 URL重定向
错误示范:
String url = request.getParameter("url");
response.sendRedirect(url); // 直接重定向
修复:
String url = request.getParameter("url");
String host = new URL(url).getHost(); // 提取host
if (ALLOWED_DOMAINS.contains(host)) { // 白名单校验
response.sendRedirect(url);
}