1. SQL注入#
Sink点#
Statement.execute()Statement.executeQuery()Statement.executeUpdate()PreparedStatement- MyBatis
${}占位符 Hibernate.createQuery()- Spring Data JPA custom query
JdbcTemplate.update()EntityManager.createQuery()
审计检查项#
- 是否使用
PreparedStatement替代Statement - SQL语句中是否存在字符串拼接
- 用户输入是否直接进入SQL
- 是否使用
?占位符进行参数化 - 参数是否通过
setString()等方法绑定 - 所有用户输入是否都被参数化
- MyBatis是否使用
#{}而非${} - 无法参数化部分是否有白名单
- ORM框架的参数绑定方式是否正确
风险代码模式#
// 模式1:直接字符串拼接
String username = request.getParameter("username");
String sql = "SELECT * FROM users WHERE username = '" + username + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql);java// 模式2:PreparedStatement但SQL已包含拼接
String username = request.getParameter("username");
String sql = "SELECT * FROM users WHERE username = '" + username + "'";
PreparedStatement pstmt = connection.prepareStatement(sql);
// 此时参数化未起作用,SQL已被拼接java// 模式3:MyBatis使用${}
<select id="select" resultType="user">
SELECT * FROM users WHERE username = '${username}'
</select>java// 模式4:ORDER BY未验证
String orderBy = request.getParameter("orderBy");
String sql = "SELECT * FROM users ORDER BY " + orderBy;java安全实现#
// 方案1:PreparedStatement参数化
String username = request.getParameter("username");
String sql = "SELECT * FROM users WHERE username = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, username);
ResultSet rs = pstmt.executeQuery();java// 方案2:MyBatis使用#{}
<select id="select" resultType="user">
SELECT * FROM users WHERE username = #{username}
</select>java// 方案3:Hibernate参数化
String username = request.getParameter("username");
Query query = session.createQuery("FROM User WHERE username = :username");
query.setParameter("username", username);
List<User> users = query.list();java// 方案4:ORDER BY白名单
String orderField = request.getParameter("orderBy");
String[] allowedFields = {"id", "username", "email", "createTime"};
if (!Arrays.asList(allowedFields).contains(orderField)) {
throw new IllegalArgumentException("Invalid field");
}
String sql = "SELECT * FROM users ORDER BY " + orderField;java2. 命令执行#
Sink点#
Runtime.getRuntime().exec()ProcessBuilder.start()ProcessBuilder.command()
审计检查项#
- 用户输入是否进入命令参数
- 是否使用
exec(String)还是exec(String[]) - 是否通过shell执行(
/bin/sh -c) - 是否实现了命令白名单
- 参数是否进行了验证
风险代码模式#
// 模式1:用户输入直接作为命令
String cmd = request.getParameter("cmd");
Runtime.getRuntime().exec(cmd);java// 模式2:通过shell执行
String host = request.getParameter("host");
Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", "ping " + host});java// 模式3:参数通过字符串而非数组传递
String args = userInputArray[0] + " " + userInputArray[1];
new ProcessBuilder(args).start();java安全实现#
// 方案1:命令白名单
String cmd = request.getParameter("cmd");
List<String> allowedCommands = Arrays.asList("whoami", "date", "pwd");
if (!allowedCommands.contains(cmd)) {
throw new IllegalArgumentException("Command not allowed");
}
Process process = Runtime.getRuntime().exec(cmd);java// 方案2:参数白名单+数组形式
String host = request.getParameter("host");
List<String> allowedHosts = Arrays.asList("google.com", "github.com");
if (!allowedHosts.contains(host)) {
throw new IllegalArgumentException("Host not allowed");
}
ProcessBuilder pb = new ProcessBuilder(
"/bin/ping", "-c", "4", host
);
Process process = pb.start();java3. 文件上传和任意文件写入#
Sink点#
FileOutputStreamFileWriterFiles.write()Files.copy()RandomAccessFileCommonsFileUpload.FileItem.write()
审计检查项#
- 扩展名是否进行白名单验证
- 是否防止了
.jsp、.class等可执行文件 - MIME类型是否经过验证
- 文件名是否包含路径分隔符
- 是否生成了新文件名
- 最终路径是否在预期目录内
- 是否使用
getCanonicalPath()验证路径
风险代码模式#
// 模式1:直接使用上传文件名
ServletFileUpload upload = new ServletFileUpload(factory);
List<FileItem> items = upload.parseRequest(request);
for (FileItem item : items) {
if (!item.isFormField()) {
String filename = item.getName();
String filePath = "uploads/" + filename;
item.write(new File(filePath));
}
}java// 模式2:仅检查扩展名
String filename = item.getName();
if (!filename.endsWith(".jpg")) {
return;
}
// 但shell.jpg.jsp可绕过java安全实现#
public void handleFileUpload(HttpServletRequest request) throws Exception {
ServletFileUpload upload = new ServletFileUpload(factory);
List<FileItem> items = upload.parseRequest(request);
for (FileItem item : items) {
if (item.isFormField()) continue;
if (item.getSize() > 5 * 1024 * 1024) {
throw new Exception("File too large");
}
String filename = item.getName();
String extension = FilenameUtils.getExtension(filename);
List<String> allowedExts = Arrays.asList("jpg", "jpeg", "png", "gif");
if (!allowedExts.contains(extension.toLowerCase())) {
throw new Exception("File type not allowed");
}
String contentType = item.getContentType();
List<String> allowedMimes = Arrays.asList("image/jpeg", "image/png");
if (!allowedMimes.contains(contentType)) {
throw new Exception("Invalid MIME type");
}
String safeFilename = UUID.randomUUID() + "." + extension.toLowerCase();
String uploadDir = "/absolute/path/to/uploads";
String filepath = new File(uploadDir, safeFilename).getCanonicalPath();
String canonicalDir = new File(uploadDir).getCanonicalPath();
if (!filepath.startsWith(canonicalDir)) {
throw new Exception("Invalid file path");
}
item.write(new File(filepath));
}
}java4. 反序列化漏洞#
Sink点#
ObjectInputStream.readObject()ObjectInputStream.readUnshared()
审计检查项#
- 是否对不可信输入执行反序列化
- 数据来源是否可信
- 是否实现了类白名单
- 是否使用
ObjectInputFilter
风险代码模式#
// 模式1:直接反序列化用户输入
byte[] data = Base64.getDecoder().decode(request.getParameter("data"));
ObjectInputStream ois = new ObjectInputStream(
new ByteArrayInputStream(data)
);
Object obj = ois.readObject();java// 模式2:从HTTP请求反序列化
ObjectInputStream ois = new ObjectInputStream(
request.getInputStream()
);
Object obj = ois.readObject();java安全实现#
// 方案1:使用ObjectInputFilter
public Object safeDeserialize(byte[] data) throws Exception {
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"java.util.*;java.lang.*;!java.io.*;!sun.*;!com.sun.*"
);
ObjectInputStream ois = new ObjectInputStream(
new ByteArrayInputStream(data)
);
ObjectInputFilter.Config.setObjectInputFilter(ois, filter);
return ois.readObject();
}java// 方案2:白名单实现
public class WhitelistObjectInputStream extends ObjectInputStream {
private static final Set<String> ALLOWED_CLASSES = new HashSet<>(
Arrays.asList("User", "Product", "Order")
);
public WhitelistObjectInputStream(InputStream in) throws IOException {
super(in);
}
@Override
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
if (!ALLOWED_CLASSES.contains(desc.getName())) {
throw new InvalidClassException("Class not allowed");
}
return super.resolveClass(desc);
}
}java5. 任意文件读取#
Sink点#
FileInputStreamFileReaderFiles.readAllBytes()Files.readAllLines()RandomAccessFile
审计检查项#
- 用户参数是否直接作为文件路径
- 是否使用
getCanonicalPath()规范化 - 最终路径是否在允许目录内
- 是否实现了文件白名单
安全实现#
public String readFileSafely(String filename) throws Exception {
String cleanFilename = new File(filename).getName();
String baseDir = "/absolute/path/to/files";
String filepath = new File(baseDir, cleanFilename).getCanonicalPath();
String canonicalDir = new File(baseDir).getCanonicalPath();
if (!filepath.startsWith(canonicalDir + File.separator)) {
throw new Exception("Access denied");
}
return new String(Files.readAllBytes(Paths.get(filepath)));
}java6. 路径遍历#
Sink点#
- 文件操作
审计检查项#
- 路径中是否包含
.. - 是否使用
getCanonicalPath()规范化 - 规范化后路径是否在基础目录内
安全实现#
public String validatePath(String userInput) throws Exception {
String baseDir = new File("/data").getCanonicalFile().getAbsolutePath();
File file = new File(baseDir, userInput).getCanonicalFile();
if (!file.getAbsolutePath().startsWith(baseDir)) {
throw new IllegalArgumentException("Path traversal detected");
}
return file.getAbsolutePath();
}java7. XXE (XML External Entity)#
Sink点#
DocumentBuilder.parse()SAXParserFactory.newInstance()XMLReader.parse()Unmarshaller.unmarshal()
审计检查项#
- 是否对不可信XML进行解析
- 外部实体是否被禁用
- DTD处理是否被禁用
XIncludeAware是否为false
安全实现#
public Document parseXmlSafely(InputStream xmlInput) throws Exception {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
DocumentBuilder db = dbf.newDocumentBuilder();
return db.parse(xmlInput);
}java8. SSRF (Server-Side Request Forgery)#
Sink点#
HttpURLConnection.openConnection()URLConnection.getInputStream()HttpClient.execute()RestTemplate.exchange()
审计检查项#
- 是否允许用户指定URL
- 是否检测内部地址
- 是否限制了协议
- 是否防止了DNS重绑定
安全实现#
public String fetchUrlSafely(String urlStr) throws Exception {
URL url = new URL(urlStr);
if (!url.getProtocol().matches("^https?$")) {
throw new IllegalArgumentException("Invalid protocol");
}
InetAddress addr = InetAddress.getByName(url.getHost());
if (addr.isLoopbackAddress() || addr.isLinkLocalAddress() ||
addr.isSiteLocalAddress()) {
throw new IllegalArgumentException("Internal address not allowed");
}
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(10000);
conn.setReadTimeout(10000);
BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream())
);
StringBuilder result = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
result.append(line);
}
return result.toString();
}java9. 表达式注入#
SpEL注入#
Sink点:ExpressionParser.parseExpression()
检查项:用户输入是否直接作为表达式;是否使用SimpleEvaluationContext
OGNL注入#
Sink点:Ognl.parseExpression()
检查项:是否允许用户输入作为OGNL表达式
安全实现(SpEL)#
String expression = request.getParameter("expr");
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context =
SimpleEvaluationContext.forReadOnlyDataBinding().build();
Expression exp = parser.parseExpression(expression);
Object result = exp.getValue(context);java10. 其他漏洞#
JNDI注入#
Sink点:InitialContext.lookup()
检查项:用户输入是否被用作JNDI名称
日志注入#
Sink点:logger.info()、logger.debug()
检查项:是否记录了未清理的用户输入;是否使用了参数化日志
LDAP注入#
Sink点:DirContext.search()
检查项:是否转义了LDAP特殊字符
11. 审计工具#
- 静态分析:CodeQL、Semgrep、SonarQube、Fortify
- 动态检测:OWASP ZAP、Burp Suite、AppScan
- 依赖检查:OWASP Dependency-Check、Snyk