0xd00 随笔小记

Back

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;
java

2. 命令执行#

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();
java

3. 文件上传和任意文件写入#

Sink点#

  • FileOutputStream
  • FileWriter
  • Files.write()
  • Files.copy()
  • RandomAccessFile
  • CommonsFileUpload.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

安全实现#


4. 反序列化漏洞#

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

5. 任意文件读取#

Sink点#

  • FileInputStream
  • FileReader
  • Files.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)));
}
java

6. 路径遍历#

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();
}
java

7. 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);
}
java

8. SSRF (Server-Side Request Forgery)#

Sink点#

  • HttpURLConnection.openConnection()
  • URLConnection.getInputStream()
  • HttpClient.execute()
  • RestTemplate.exchange()

审计检查项#

  • 是否允许用户指定URL
  • 是否检测内部地址
  • 是否限制了协议
  • 是否防止了DNS重绑定

安全实现#


9. 表达式注入#

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);
java

10. 其他漏洞#

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
Java 代码审计 - 漏洞Sink点
https://blog.0xd00.com/blog/java_audit
Author 0xd00
Published at 2025年12月18日
Comment seems to stuck. Try to refresh?✨