Web容器是承载Web应用运行的核心环境,其自身的安全性和应用程序的代码安全共同构成了Web安全防御的重要战线。本文将深入解析Java Web的演进与核心架构,并结合实战案例剖析常见安全漏洞的审计方法与Apache服务器经典配置漏洞。
Java Web发展历程与技术演进
初代MVC架构
MVC(Model-View-Controller)是一种经典的设计模式,它将应用程序分为三个核心部分:
- V(View):代表视图,主要负责页面的渲染。
- C(Controller):代表控制层,处理页面跳转、业务逻辑,以及访问数据库等操作。
- M(Model):代表模型,通常是一个类,对应从数据库取出或将要存入数据库的数据。

除了示意图中的Applet,其他技术至今仍广泛应用于互联网,并持续面临着各自的安全威胁。
关键技术的发展节点
Java Web技术栈经历了一系列关键演进:

① Applet 与 Servlet
- Applet:一种运行在客户端浏览器中的Java程序,可视为一种支持Java的网页插件。
- Servlet:用Java编写的服务器端程序,用于动态生成Web内容。
两者的核心区别在于,Applet拥有用户界面类,而Servlet没有界面,它负责接收并处理客户端的请求。
② 从 Servlet 到 JSP
直接使用Servlet输出复杂的HTML页面代码非常繁琐。于是,人们思考:能否直接在HTML中编写Java代码?基于这个想法,JSP(Java Server Pages)诞生了。
JSP本质上也是一种Servlet。当用户访问JSP页面时,它会被编译成由Java代码编写的Servlet,再生成响应返回给客户端。
③ 从 JSP 到 Struts1
在JSP中,页面代码和业务逻辑代码混合在一起,导致业务变动时需要修改大量JSP页面,维护困难。因此,Struts1框架应运而生。
它将JSP作为视图(View)进行界面展示,实现了与业务逻辑的分离,并明确提出了MVC(Model-View-Controller)架构,在当时非常流行,约在2004年达到使用巅峰。但Struts1也有明显缺点:代码严重依赖其自身API,属于侵入式框架,且视图层与控制层耦合较紧密。
④ 从 Struts1 到 Spring MVC & Spring Boot
随后,Spring开源社区推出了Spring MVC,它迅速成为最优秀且最受欢迎的框架。但其问题在于配置非常复杂,需要编写大量配置文件。
为了简化配置,Spring Boot被提出,它遵循“约定大于配置”的理念,极大地简化了项目搭建和配置过程。
⑤ 从单体架构到微服务
然而,即使是基于Spring Boot,在大型项目中,所有代码通常部署在一起(单体架构)。这导致更新某个模块或修复某个Bug时,需要停用整个项目。为了减少停机时间、实现快速更新,目前的主流方向转向了Spring Cloud Microservice(微服务)架构。
Java Web应用与Spring MVC工作流程
Servlet示例
一个典型的Servlet类结构如下,它扩展了HttpServlet,并通过doGet和doPost方法分别处理GET和POST请求。

当收到GET请求时,会进入doGet方法处理;收到POST请求时,则进入doPost方法处理。其中,请求报文被封装在HttpServletRequest对象中,而返回报文则封装在HttpServletResponse对象里。
配置文件映射
web.xml配置文件负责建立URL到具体Servlet的映射关系,主要包含<servlet>和<servlet-mapping>两部分。

- 首先,将Servlet名称(
<servlet-name>)与实际处理请求的Java类(<servlet-class>)绑定。
- 然后,将URL访问路径(
<url-pattern>)与Servlet名称(<servlet-name>)绑定。
通过这两层绑定,就实现了从浏览器访问的URL到具体后端处理类代码的映射。

配置之后,就可以在浏览器中通过URL直接访问到对应的Servlet功能。

Spring MVC工作流程
Spring MVC框架处理请求的过程更为精细,其核心是DispatcherServlet。

- 浏览器向后端发起HTTP请求。
- 请求首先进入
DispatcherServlet,它继承自HttpServlet。
DispatcherServlet会通过HandlerMapping寻找并确定需要调用哪个控制器(Controller)。
- 找到对应的Controller后,控制器会调用模型(Model)来处理具体的业务逻辑。
- 业务处理完成后,返回一个
ModelAndView对象给DispatcherServlet。
DispatcherServlet再通过ViewResolver(视图解析器)来处理视图映射。
- 映射关系确定后,
DispatcherServlet将模型数据(Model)传递给视图(View)进行渲染。
- 最后,生成HTML页面返回给浏览器。
这就是一个完整的Spring MVC请求处理流程。

从继承关系图中可以看到,Spring MVC的类层次结构较为复杂,并且传统配置需要大量XML文件。因此,当前的主流趋势是采用基于Spring Boot的微服务架构。

实际上,每个Spring微服务内部都是一个Spring Boot应用。而Spring Boot正是基于Spring MVC标准演化而来的一种架构,它主要采用“约定大于配置”的方式,极大地减少了配置文件的编写工作量。
源代码审计实战方法
软件代码审计是对编程项目中的源代码进行的全面分析,主要目的是发现错误、安全漏洞或违反编程约定的情况。它是防御性编程的组成部分,旨在软件发布前尽可能减少错误。
审计方式与要点
审计之前,最好创建一个核心问题清单,包括安全漏洞、身份验证问题、授权问题、内存泄漏和不良设计习惯之类的问题。其中,跟踪用户输入数据和敏感函数参数回溯,是最为常用的审计方法。
在审计源码之前,首先要对项目建立宏观概念,包括其核心功能、支撑的业务以及内部的源码结构。

宏观上,可以观察项目的包结构来理解其组织方式。

例如:
common包通常存放公共类。
config包主要存放配置类。
controller包对应控制层。
resources目录则用于存放静态文件、配置文件等资源。
不同项目的包结构可能略有差异,但整体思想一致。建立宏观概念后,就可以针对具体的漏洞类别进行指向性分析。
常见漏洞类型及审计思路
① 密码硬编码与明文存储
这类问题常出现在属性文件、Java源码或XML等配置文件中。例如,在数据库连接配置中直接硬编码用户名和密码。
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>
@Bean
public DataSource getDataSource() {
DataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/contactdb");
dataSource.setUsername("root");
dataSource.setPassword("Password");
return dataSource;
}
审计时,应全局搜索相关关键词(如 password、username、jdbc 等),检查敏感信息是否以明文形式暴露。
② 命令注入
关注代码中执行系统命令的位置。常见的关键词和类包括 Runtime.exec()、ProcessBuilder、getRuntime() 以及 shell 等。
public static void method1(String cmd) {
try {
Process process = Runtime.getRuntime().exec(cmd);
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String s;
while((s=reader.readLine())!=null) {
System.out.println(s);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void method2(String cmd) {
try {
ProcessBuilder processBuilder = new ProcessBuilder(cmd);
processBuilder.redirectErrorStream(true);
Process p = processBuilder.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
String s=null;
while((s=reader.readLine())!=null) {
System.out.println(s);
}
} catch (IOException e) {
e.printStackTrace();
}
}
若发现这些调用,需重点审计执行参数是否用户可控,以及是否存在绕过可能。
③ 文件上传漏洞
首先定位项目中所有潜在的文件上传点。审计重点在于验证逻辑,尤其是对用户输入的白名单过滤机制是否可被绕过。
@PostMapping("/upload")
def upload_file(request):
if request.method == "POST":
file = request.FILES.get("file")
if not file:
return JsonResponse({"code": 400, "msg": "文件未上传"})
# 获取文件名
filename = file.name
# 设置文件保存路径
file_directory = os.path.join(settings.MEDIA_ROOT, "uploads")
if not os.path.exists(file_directory):
os.makedirs(file_directory)
# 构建文件保存路径
file_path = os.path.join(file_directory, filename)
# 写入文件
with open(file_path, "wb+") as destination:
for chunk in file.chunks():
destination.write(chunk)
# 返回成功响应
return JsonResponse({"code": 200, "msg": "文件上传成功", "file_url": settings.MEDIA_URL + "uploads/" + filename})
else:
return JsonResponse({"code": 405, "msg": "不支持的请求方法"})
相关关键词包括 upload、multipart、file、fileName、filePath 等。
④ 越权与URL跳转
寻找进行重定向或转发的函数,如 sendRedirect()、setHeader()、forward() 等。
public class AdminLoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestPath = request.getRequestURI();
if (requestPath.startsWith("/admin") && !request.getSession().getAttribute("loginUser") != null) {
response.sendRedirect("/admin/login");
return false;
}
return true;
}
}
检查这些操作涉及的路径或参数是否用户可控,从而可能导致未授权访问或恶意跳转。
⑤ 其他常见漏洞关键词
- SQL注入:
Select, Dao, from, delete, update, insert, createStatement
- 反序列化:
readObject, readUnshared, JSON.parseObject
- XSS:
getParamter, param
- XML注入:
XMLStreamReader, SAXReader, XMLReader 等
审计工具
除了手工审计,还可以借助自动化工具提高效率。例如 Fortify SCA,它是一款静态应用程序安全性测试(SAST)产品,可用于分析源代码,检测安全漏洞。
Apache服务器安全配置漏洞
从安全角度审计Apache服务器,有两个主要方向:Apache自身配置的安全性问题,以及其本身存在的已知漏洞(Nday)。
多后缀解析漏洞
Apache有一个特性:通过使用AddHandler指令(例如 AddHandler application/x-httpd-php .php),可以为特定文件扩展名指定处理程序。关键在于,Apache在识别文件扩展名时,会从前向后进行匹配。当遇到无法识别的扩展名时,它会继续向后查找,直到遇到第一个可识别的扩展名,并以此作为该文件的最终类型。

案例解析
假设上传一个名为 test.php.xyz.jpg 的文件,其内容为 <?php phpinfo(); ?>。首次访问时,它可能被当作图片处理。

根据Apache的多后缀解析特性:对于文件名test.php.xyz.jpg,最后一个扩展名.jpg可能是Web应用允许上传的类型,而第一个扩展名.php是Apache可识别的脚本类型。因此,只要Apache配置允许多后缀解析,该文件就可能被当作PHP脚本执行。
配置方式有两种:
-
修改Apache主配置文件(httpd.conf等)并重启服务。

-
使用更灵活的.htaccess文件,对当前目录下的文件应用修改。

假设采用第二种方式配置后,再次访问test.php.xyz.jpg文件,该文件已被成功解析并执行其中的PHP代码。

Apache Nday漏洞:CVE-2017-15715解析漏洞
Nday漏洞是指已经公开N天的漏洞,与之相对的是尚未公开的0day漏洞。CVE-2017-15715是一个影响Apache 2.4.10-2.4.29版本的换行解析漏洞。

漏洞复现
假设有一个简单的文件上传页面。

后端采用黑名单过滤,拦截扩展名为 .php、.php5、.phtml 或 .pht 的文件。
if(in_array($ext, ['php', 'php3', 'php4', 'php5', 'phtml', 'pht'])) {
exit('bad file');
}

直接上传1.php文件会被拦截。


绕过尝试
在Burp Suite中修改上传请求,将文件名 1.php 末尾(在filename字段的值中)插入一个换行符(十六进制0A)。

这次上传成功,没有收到“bad file”错误。访问上传后的文件,发现它已经被Apache成功解析执行。

漏洞原理
我们实际上传的文件后缀变为了.php%0A(%0A是换行符的URL编码)。黑名单禁止的后缀是.php、.php5、.phtml和.pht,因此.php%0A成功绕过了黑名单检查。

Apache开发者在编写用于匹配文件类型的正则表达式时,使用了类似\.php$的模式(即以.php结尾)。这里的$符号在正则表达式中匹配“行尾”。而换行符0A正好被解释为行尾。因此,Apache在进行匹配时,会将1.php%0A识别为以.php结尾的文件,从而调用PHP解析器来处理该文件。

这个漏洞的本质是黑名单过滤逻辑与Apache解析器对文件名解释方式的不一致,从而被绕过。理解和掌握这类底层原理,是进行有效安全代码审计和渗透测试的关键。
希望这篇结合了架构解析与实战案例的文章,能帮助你在云栈社区以及其他技术场景中,更深入地理解Web容器层面的安全风险与防御要点。