ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MySQL空值判断:从IS NULL到IFNULL的防注入与挖洞实战

MySQL空值判断:从IS NULL到IFNULL的防注入与挖洞实战 之前带学员做 SRC 漏洞挖掘练习时很多刚接触网络安全的朋友都会问同一个问题“我连 MySQL 查询都不熟练真的能去做挖洞实战吗”我的回答通常是恰恰因为 SQL 基础不牢很多人在测试注入点和判断业务逻辑时才会毫无头绪。SQL 注入只是攻击手法而读数据、过滤条件、判断空值才是你手里真正的基础武器。本篇文章是“零基础入门到 SEC 挖洞实战”系列的第 24 篇。我会聚焦 MySQL 条件查询中一个既基础又容易踩坑的话题判断字段是否为空。内容会围绕 IS NULL、空字符串、IFNULL、COALESCE、动态 WHERE 条件拼接展开同时结合网络安全从业者的视角分析这些查询写法在漏洞挖掘、绕过防护和代码审计中可能遇到的问题。如果你是非科班转行网络安全或者刚开始接触 SRC挖洞平台想系统补一遍 MySQL 基础那么这篇文章非常适合你。本文会给出大量可直接执行的 SQL 示例也会提醒你哪些写法在“挖洞视角”下特别危险。1. 为什么学渗透测试还要熟练 MySQL 条件查询1.1 从挖洞实战看 SQL 基础的价值很多刚开始接触网络安全的人会有一个误区觉得“挖洞”就是拿到一个 URL然后用扫描器跑一遍看到漏洞直接提交报告。但真实场景中无论是手工验证 SQL 注入还是分析一个业务接口是否存在越权都要求你能够理解后端的查询逻辑。举个例子一个商城网站的搜索框本质可能执行了类似下面的 SQLSELECT * FROM products WHERE name LIKE %手机% AND status 1;如果你在搜索框输入一个单引号页面报出数据库错误你能否判断它拼接 SQL 的方式如果后端代码用WHERE name 用户输入 AND delete_flag IS NULL这样的逻辑你又能否通过参数改变判断条件这些都依赖你对 MySQL 条件查询有足够深的理解。另外在 SRC 漏洞挖掘中很多逻辑漏洞来自开发者没有正确处理“空值”与“空字符串”。同一个字段可能在某些情况下是 NULL另一些情况下是空字符串。如果查询条件判断不严谨就可能出现数据越权访问或者业务逻辑绕过。1.2 MySQL 条件查询在整个学习路线中处于什么位置对于零基础入门的读者MySQL 的学习路径通常建议按照下面这个顺序展开安装 MySQL 并掌握基本连接命令。学会建库、建表、插入基础数据。掌握 SELECT 查询和 WHERE 条件过滤。掌握聚合、排序、分组。掌握多表 JOIN 连接查询。理解事务、索引、权限。结合 Web 应用学习如何安全地拼接 SQL。本文讨论的“判断是否为空”属于第 3 阶段中一个比较细的分支。它不复杂但坑极多。很多人在这个点上出了问题不是因为不知道IS NULL这个语法而是因为在真实业务中运行了几年、存了几百万条数据的表里NULL和空字符串几乎总是混合存在的。1.3 为什么要单独用一篇文章讲“为空”判断MySQL 中的“空值”并不真的是“什么都没有”。从底层存储来看NULL是一个特殊标记它表示“未知”或“不存在”而空字符串是一个真实的字符串值长度为 0。这两种状态的判断方式完全不同这也是最容易出问题的地方。如果不加区分地使用 NULL或者! NULL查询结果会直接让你怀疑人生。后面会专门演示这些错误写法并给出正确写法。2. 环境准备与基础表设计2.1 环境说明本文的 SQL 示例以 MySQL 8.x 为例同时也兼容 MySQL 5.7 的绝大部分写法。如果你使用的是 MariaDB本节的 SQL 也可以直接运行。版本需要根据你的实际项目做调整本文重点演示 SQL 查询逻辑不在安装和版本差异上过多展开。如果你的电脑还没有安装 MySQL可以从官网下载社区版也可以使用 Docker 快速拉起一个临时环境docker run --name mysql-study -e MYSQL_ROOT_PASSWORD123456 -p 3306:3306 -d mysql:8.0启动容器后可以使用下面的命令进入 MySQL 交互界面docker exec -it mysql-study mysql -uroot -p123456再次说明这只是为了方便本地练习。生产环境中数据库密码策略、权限管理必须按照企业的安全规范来执行不能在公网环境随意暴露端口。2.2 创建一张“用户表”作为演示样本为了更贴近网络安全学习中常见的“用户中心”“订单系统”场景我们创建一张用户扩展信息表。这张表除了常见字段外特别加入了几个容易出现 NULL 和空字符串问题的字段。CREATE DATABASE IF NOT EXISTS sec_demo DEFAULT CHARSET utf8mb4; USE sec_demo; DROP TABLE IF EXISTS t_user_profile; CREATE TABLE t_user_profile ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, username VARCHAR(50) NOT NULL COMMENT 用户名, nickname VARCHAR(50) DEFAULT NULL COMMENT 昵称, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, email VARCHAR(100) DEFAULT NULL COMMENT 邮箱, intro TEXT COMMENT 个人简介, delete_flag TINYINT DEFAULT 0 COMMENT 删除标记0表示未删除1表示已删除 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT 用户扩展信息表;解释一下字段设计思路username使用NOT NULL因为注册时一定有用户名这是强约束。nickname、phone、email使用DEFAULT NULL表示“用户可能没填”。intro是可空文本字段适合模拟“个人简介为空”的情况。delete_flag是软删除标记这是很多业务系统里都会出现的字段后面在“动态条件查询”和“挖洞视角”中会重点讨论。2.3 写入测试数据插入下面的测试数据方便后续验证不同查询结果INSERT INTO t_user_profile (username, nickname, phone, email, intro, delete_flag) VALUES (zhangsan, 张三, 13800001111, zhangsanexample.com, 网络安全爱好者, 0), (lisi, NULL, NULL, NULL, NULL, 0), (wangwu, , 13900002222, , , 0), (zhaoliu, 赵六, NULL, zhaoliuexample.com, 测试账号, 1), (sunqi, , NULL, sunqiexample.com, NULL, 0);现在表里的数据状态很有趣lisi的 nickname、phone、email 都是 NULL。wangwu的 nickname 是空字符串email、intro 也是空字符串。sunqi的 nickname 是空字符串intro 却是 NULL。zhaoliu是已删除用户delete_flag 1。这些混在一起的数据就是实际项目里最常见的状态。接下来我们开始查询。3. 判断字段是否为 NULLIS NULL 和 IS NOT NULL3.1 错误的“ NULL”写法初学 MySQL 时几乎每个人都写过这样的 SQL-- 错误示例查不到任何数据 SELECT * FROM t_user_profile WHERE nickname NULL;执行结果是什么没有任何数据返回也不会报错。原因是 MySQL 中NULL不是一个值而是一个“未知”的状态。是值之间的相等比较当事务一方是未知时整个表达式的结果也是未知WHERE 子句只会留下结果为 TRUE 的行所以查询结果为空。同样下面这条 SQL 也是错误的-- 错误示例会返回所有非NULL行但这里不是说“没有昵称” SELECT * FROM t_user_profile WHERE nickname ! NULL;3.2 正确的 IS NULL 写法要判断字段是否为 NULL必须使用IS NULLSELECT id, username, nickname, phone, email FROM t_user_profile WHERE nickname IS NULL;执行结果预期为idusernamenicknamephoneemail2lisiNULLNULLNULL同理判断“不为空”应该使用IS NOT NULLSELECT id, username, nickname FROM t_user_profile WHERE nickname IS NOT NULL;这一步很容易理解但需要养成肌肉记忆看见 NULL 判断第一反应就是IS NULL/IS NOT NULL不要用或!。3.3 挖洞视角IS NULL 和权限绕过场景了解基础语法后我们把它放进安全场景。假设某个网站的后台系统在列出用户时用了这样的 SQLSELECT * FROM users WHERE delete_flag IS NULL OR delete_flag 0;这种写法常见的背景是早期代码把delete_flag的默认值设计成了 NULL后来为了统计方便才补上默认 0。如果开发者意识不统一有的行是 NULL有的行是 0那么查询就必须写成IS NULL OR 0。从渗透测试角度看遇到这类查询时你在请求参数里看到delete_flag0但系统内部实际处理 NULL 的逻辑你无法直接看到。需要观察是否存在“参数缺省”的情况如果不传 delete_flag后端代码是把它当 NULL 还是当 0这往往就是越权访问或水平权限漏洞的诞生点。不过要特别提醒这属于授权测试或漏洞挖掘练习时的分析思路。你不能在未授权的系统上做任何验证必须遵守法律和平台规则。4. 判断空字符串 与 CHAR_LENGTH4.1 空字符串是“长度为0的字符串”空字符串是一个真实存在的值。它既不是 NULL也不是“没有内容”。比如用户提交表单时如果前端把输入框里的内容清空而后端没有做拦截保存到数据库里的往往就是。下面的查询可以找出“昵称为空字符串”的用户SELECT id, username, nickname FROM t_user_profile WHERE nickname ;预期结果是idusernamenickname3wangwu5sunqi4.2 同时过滤 NULL 和空字符串IFNULL 结合条件但现实业务中用户表里的“没填昵称”状态可能是 NULL也可能是空字符串。如果只判断nickname 就漏掉了 NULL 的记录如果只判断IS NULL就漏掉了空字符串的记录。常见的处理方式是用IFNULL把 NULL 转换成空字符串再统一比较SELECT id, username, nickname FROM t_user_profile WHERE IFNULL(nickname, ) ;这条 SQL 的执行过程是IFNULL(nickname, )表示如果 nickname 是 NULL则返回空字符串否则返回 nickname 本身。这样一来无论是 NULL 还是空字符串都会在比较前被统一成。再和比较时就能把两种“为空”状态全部找出来。结果为 lisi、wangwu、sunqi 三条记录。也可以使用COALESCE它是更通用的空值合并函数可以传多个参数返回第一个非 NULL 值SELECT id, username, nickname FROM t_user_profile WHERE COALESCE(nickname, ) ;4.3 CHAR_LENGTH 判断空内容如果文本字段里可能包含空格比如用户填了一个空格“ ”作为昵称那么nickname 也无法捕捉到。更严格的业务校验通常会使用TRIM去掉首尾空格后再判断SELECT id, username, nickname FROM t_user_profile WHERE CHAR_LENGTH(TRIM(nickname)) 0;这条 SQL 的逻辑是TRIM(nickname)去掉首尾空格。CHAR_LENGTH返回字符串长度。长度等于 0说明内容是空字符串、NULL 或纯空格。但要注意CHAR_LENGTH(TRIM(NULL))的结果是 NULL而不是 0。因此如果字段本身可能是 NULL这种方式无法直接筛出 NULL 记录。最稳妥的方式还是先IFNULL(nickname, )再 TRIM 再计算长度SELECT id, username, nickname FROM t_user_profile WHERE CHAR_LENGTH(TRIM(IFNULL(nickname, ))) 0;这条 SQL 可以查出来“实际用户看起来没有昵称”的所有记录不管是 NULL、空字符串还是纯空格这是很实用的写法。5. NOT IN 与 NULL 的隐藏陷阱5.1 NOT IN 遇到 NULL 为什么会失效这是很多后端开发者和安全测试人员都踩过的坑。先看一个例子假设要查询所有“邮箱不是 zhangsanexample.com 和 lisiexample.com”的用户SELECT id, username, email FROM t_user_profile WHERE email NOT IN (zhangsanexample.com, lisiexample.com);直觉上你可能会觉得返回结果应该是不包含这两条记录的所有用户。但在 MySQL 中这条 SQL 的返回结果会让人困惑因为凡是 email 为 NULL 的行都不会被返回。为什么因为 NOT IN 本质上等价于多个AND email ! zhangsanexample.com AND email ! lisiexample.com。如果 email 是 NULL那么NULL ! xxx的结果是 NULL不是 TRUE所以整行无法通过 WHERE 过滤。为了避免这种问题需要显式排除 NULL或使用IFNULL做转换SELECT id, username, email FROM t_user_profile WHERE IFNULL(email, ) NOT IN (zhangsanexample.com, lisiexample.com);这样 email 为 NULL 的行会被当成空字符串参与比较从而返回在结果集中。5.2 挖洞视角NOT IN 导致数据漏查在授权漏洞测试中如果你发现某个业务接口的“黑名单”功能没有生效可以猜一下后端是不是用了这种写法SELECT * FROM user_blacklist WHERE username NOT IN (admin, test);假设被测试的目标系统里用户名允许为 NULL虽然不合理但实际中常见那么 NULL 用户会绕过黑名单过滤。这个场景常被用来解释为什么很多“逻辑漏洞”不是出在特别高深的地方而是出在最基础的 SQL 语义理解上。当然真正做 SRC 挖掘时你不能拿别人的线上库来做实验。理解这条规则的目的是当你阅读目标系统的开源代码或者在授权测试中看到类似的 SQL 片段时能快速判断它是否存在可利用的过滤绕过点。6. 动态拼接 WHERE 条件的实现思路与安全问题6.1 需求场景很多网安学习者一开始只是单纯学 SQL但进入 SRC 挖洞实战后会遇到一个高频关键词动态拼 WHERE 查询条件。具体来说前端页面上有多个筛选条件用户名、邮箱、手机号、是否删除。用户可以填其中一个也可以同时填多个也可以什么都不填。后端需要根据用户实际传入的条件动态生成 SQL。-- 伪代码动态拼接 WHERE username xxx AND email yyy AND phone zzz如果没有填某个条件就不要把它拼接进 WHERE 中否则查询结果就是空。这里的关键点有两个拼接前需要判断参数是否为空。判断“空”时同样要区分 NULL、空字符串、空格等情况。6.2 一条完整的动态查询示例假设我们要实现的接口是根据可选的 username、nickname、phone 查询未删除的用户。以下是 Java 后端中 MyBatis 动态 SQL 的一个常见写法同时也代表了一种安全实践——使用#{}预编译参数避免 SQL 注入。!-- 文件路径src/main/resources/mapper/UserProfileMapper.xml -- select idsearchUsers resultTypecom.example.demo.entity.UserProfile SELECT id, username, nickname, phone, email FROM t_user_profile where if testusername ! null and username ! AND username #{username} /if if testnickname ! null and nickname ! AND nickname #{nickname} /if if testphone ! null and phone ! AND phone #{phone} /if AND delete_flag 0 /where /select这里有几个点值得说明where标签会自动处理开头的 AND如果所有条件都不满足它不会生成无意义的 WHERE 子句。if用来做空值判断避免把用户没填的字段拼进 SQL。delete_flag 0直接写死保证只查未删除数据。使用#{username}是预编译参数方式而不是${username}字符串拼接。这是防 SQL 注入的基本要求。如果你看到的代码中使用的是${}并且把用户输入直接拼进 SQL那么漏洞风险非常高。6.3 空值判断在动态条件里的坑上面 MyBatis 的if testnickname ! null and nickname ! 只能过滤掉 Java 中的 null 和空字符串无法过滤全空格字符串。如果要严格处理可以写成if testnickname ! null and nickname.trim() ! 这里的.trim()会先去掉首尾空格再判断是否为空。你还需要在 Service 层或前端做类似处理不能只依赖数据库层。其实动态 SQL 不止存在于 MyBatis 中。在很多小型 Web 项目中开发者喜欢直接在 Java 中用 StringBuilder 拼接 SQL比如String sql SELECT * FROM users WHERE 11 ; if (username ! null !username.isEmpty()) { sql AND username username ; }这段代码最大的问题不是 WHERE 11而是直接拼接用户输入。username 一旦包含单引号就可能被注入。安全做法是手写 PreparedStatement而不是在 Java 中拼 SQL 字符串。下面给一个 Java 原生 PreparedStatement 的示例// 文件路径src/main/java/com/example/demo/UserSearchService.java public ListUser searchUsers(String username, String phone) { StringBuilder sql new StringBuilder(SELECT * FROM t_user_profile WHERE delete_flag 0 ); ListObject params new ArrayList(); if (username ! null !username.trim().isEmpty()) { sql.append( AND username ? ); params.add(username.trim()); } if (phone ! null !phone.trim().isEmpty()) { sql.append( AND phone ? ); params.add(phone.trim()); } try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql.toString())) { for (int i 0; i params.size(); i) { ps.setObject(i 1, params.get(i)); } try (ResultSet rs ps.executeQuery()) { // 处理结果集 } } catch (SQLException e) { // 记录日志并做异常处理不要把异常详情返回给前端 } return null; }用?占位符配合setObject填充参数可以避免 SQL 注入。另一种思路是使用 MyBatis 的where和if它生成的 SQL 仍然是预编译形式只要不误用${}安全性就有保障。6.4 动态条件查询为什么要注意 delete_flag在数据库实战和挖洞实战中delete_flag或is_deleted非常常见。很多系统采用软删除方案删除操作只把 delete_flag 置为 1而不是真正 DELETE 数据。如果后端查询时漏掉了AND delete_flag 0那么已被删除的数据依然会通过查询接口被返回。在越权测试、IDOR不安全的直接对象引用测试中这种漏洞经常被利用。所以无论你是在做正常开发还是在进行网络安全测试都建议先问一个问题这个表有没有软删除字段每次查询是否都正确携带了未删除条件是否存在某个查询接口会泄露已删除的数据这些问题对了解一个系统是否安全非常有帮助。7. 基于条件判断的 SQL 注入注入点分析7.1 什么是条件语义被改变在 SQL 注入中有一类常见的利用思路是“改变 WHERE 条件的判断语义”。举个典型例子下面这条查询原本是想查到指定用户SELECT * FROM users WHERE username admin AND password 123456;如果后端代码用字符串拼接String sql SELECT * FROM users WHERE username username AND password password ;当用户输入username admin --时SQL 变成SELECT * FROM users WHERE username admin -- AND password 123456;注释符把后面的密码校验条件全部注释掉于是攻击者不需要密码就能登录。这种利用的本质是“将用户输入拼接进了原本的查询条件中从而改变了整体判断逻辑”。7.2 NULL 判断与盲注再回到“判断为空”这个话题。在 SQL 盲注中有一种常见判断是让某个条件恒真或恒假来探测数据。比如AND IFNULL(nickname, ) 如果 nickname 是 NULL那么括号内的表达式为 TRUE如果 nickname 不是 NULL表达式为 FALSE。这听起来像一个普通的业务判断但在安全测试中攻击者可能用它做逐字符比较。比如AND IFNULL((SELECT SUBSTRING(database(), 1, 1)), ) s这条语句的含义是取出当前数据库名的第一个字符如果这个字符是 s则整条查询返回正常否则查询结果不同。通过页面的正常/异常差异攻击者可以一点一点猜出数据库名。这种方式属于“布尔盲注”。如果你是一个白帽测试人员理解这种原理能帮你更好地看清漏洞成因。但必须再次强调不要去未授权的网站做这类尝试。应该在本地靶场或授权测试环境中练习比如自己搭建 DVWA、SQLi-Labs 等靶场或使用 SRC 平台授权的测试项目。7.3 如何写出安全的“空值条件”代码从代码审计角度防御 SQL 注入的关键点包括永远不要手工拼接用户输入到 SQL 中。要使用参数化查询或预编译语句例如 JDBC 的 PreparedStatement、MyBatis 的#{}。在 MyBatis 中只有少数不适合参数化的场景可以使用${}比如动态表名、动态排序字段但这些字段必须走白名单校验不能直接使用用户输入。在业务代码中判断空值尽量用工具类方法统一处理不要每一处都写不同的判断逻辑。对 MySQL 条件查询来说还有一个额外的安全注意点参数化查询无法修复表名、列名层面的注入。如果你把用户传入的“排序字段”直接拼进ORDER BY ${sortField}即使用了 PreparedStatement也很难完全防御。这种场景下建议使用白名单映射。8. 完整实践一条用户筛选查询的多种正确写法8.1 需求描述在本地库sec_demo中写一个查询脚本完成以下需求支持按 nickname 是否为空筛选。支持按手机号是否为空筛选。支持按 delete_flag 是否为 0 筛选。支持查看 NULL 状态和空字符串状态的区别。最终输出一份“过滤掉无邮箱用户”的用户列表。8.2 从最简单到综合的 SQL先看最简单的只查“邮箱为空”的用户这里我们把 NULL 和空字符串都算作“空”SELECT id, username, email FROM t_user_profile WHERE IFNULL(email, ) ;这条 SQL 返回 wangwu 和 sunqi因为 lisi 的 email 是 NULLwangwu 的 email 是空字符串sunqi 的 email 不是 NULL 也不是空字符串等等我们查看插入语句lisiemail 为 NULL。wangwuemail 为 。zhaoliuemail 为 zhaoliuexample.com。sunqiemail 为 sunqiexample.com。zhangsanemail 为 zhangsanexample.com。如果你把要求改为“邮箱不为空”则可以使用SELECT id, username, email FROM t_user_profile WHERE email IS NOT NULL AND email ! ;如果把纯空格也视为违反规则则更严谨的写法是SELECT id, username, email FROM t_user_profile WHERE CHAR_LENGTH(TRIM(IFNULL(email, ))) 0;8.3 综合案例筛选出需要“人工补全资料”的用户这条综合 SQL 想要查的是昵称为空或手机号为空或邮箱为空并且还不是删除账号的用户。这是典型的运营后台需求。SELECT id, username, nickname, phone, email FROM t_user_profile WHERE delete_flag 0 AND ( CHAR_LENGTH(TRIM(IFNULL(nickname, ))) 0 OR CHAR_LENGTH(TRIM(IFNULL(phone, ))) 0 OR CHAR_LENGTH(TRIM(IFNULL(email, ))) 0 );预期结果会因为数据差异而不同。按我们插入的数据来看zhangsan资料完整不满足筛选。lisidelete_flag 0nickname、phone、email 均为 NULL符合条件。wangwudelete_flag 0nickname 为空字符串phone 有值email 为空字符串符合条件。zhaoliudelete_flag 1不会进入结果。sunqidelete_flag 0nickname 为空字符串phone 为 NULLemail 有值符合条件。所以结果应该有 lisi、wangwu、sunqi 三条。8.4 使用存储过程或脚本验证如果你使用的是命令行直接粘贴上面的 SQL 就能看到结果。如果你希望在业务代码中复用可以考虑编写一个查询函数。这里给出一个纯 SQL 封装视图的案例。CREATE OR REPLACE VIEW v_user_profile_need_fill AS SELECT id, username, nickname, phone, email FROM t_user_profile WHERE delete_flag 0 AND ( CHAR_LENGTH(TRIM(IFNULL(nickname, ))) 0 OR CHAR_LENGTH(TRIM(IFNULL(phone, ))) 0 OR CHAR_LENGTH(TRIM(IFNULL(email, ))) 0 );之后可以直接通过下面语句查询SELECT * FROM v_user_profile_need_fill;视图的好处是封装复杂逻辑调用方不需要每次都写重复判断。但要注意如果表数据量很大在视图上继续过滤时很难再有效利用普通索引。实际项目中需要测试查询计划不能盲目照搬。9. NULL、空字符串与 DISTINCT / COUNT 的组合查询9.1 COUNT 函数与 NULLMySQL 中COUNT(*)和COUNT(column)的行为不一样。COUNT(*)统计行数不管该行中某个字段是否为 NULLCOUNT(email)统计的是 email 字段非 NULL 的行数。例如SELECT COUNT(*) AS total_rows, COUNT(email) AS email_not_null, COUNT(IFNULL(email, )) AS email_not_empty FROM t_user_profile;在这个例子中COUNT(email)会忽略 email 为 NULL 的行但不会忽略空字符串行。所以 email 为 的 wangwu 会被统计进email_not_null。如果你想统计“邮箱真正填写且非空字符串”的用户就得先处理空字符串的问题。9.2 为什么挖洞测试要看 COUNT 的行为差异在信息收集阶段通过接口返回的总数可以反推数据库里的记录情况。比如一个用户列表接口返回的总数如果比实际展示数多有可能存在软删除数据被查询出来或被错误统计的情况。你甚至能通过“总数异常”去判断后端筛选条件的严谨程度。当然我不会建议你用这些技巧去做任何未授权行为。但在 SRC 漏洞挖掘平台里如果某个测试目标是你拥有授权资格的那么通过这类差异发现逻辑缺陷是常见思路。9.3 使用 COALESCE 统一处理字段统计COALESCE 函数可以一次处理多个字段返回第一个非 NULL 值。你可以用它在展示层把 NULL 转换为默认值SELECT id, username, COALESCE(nickname, 未填写昵称) AS nickname_display, COALESCE(phone, 未绑定手机) AS phone_display FROM t_user_profile;COALESCE 与 IFNULL 的区别在于IFNULL 只有两个参数而 COALESCE 可以接收多个参数。SQL 标准更推荐 COALESCE只是老代码里用得更多的是 IFNULL。两者都可以用于判断空值但要注意它们不能把空字符串自动转换为默认值。如果想处理空字符串需要结合 NULLIFSELECT id, username, COALESCE(NULLIF(TRIM(nickname), ), 未填写昵称) AS nickname_display FROM t_user_profile;这里的NULLIF(TRIM(nickname), )表示如果 nickname 去掉空格后是空字符串则返回 NULL否则返回原值。然后外层的 COALESCE 再把 NULL 转成默认文案。这条写法的好处是同时处理了 NULL、空字符串和纯空格三种情况。10. 常见问题与排查思路下面整理几张排查表先收藏后面实际使用时可以直接对照。问题现象常见原因解决思路SELECT * FROM table WHERE name NULL查不到数据把 NULL 当成普通值使用 比较改为name IS NULLSELECT * FROM table WHERE name ! NULL返回空或错误结果错误使用 ! 判断非空改为name IS NOT NULL查“没有昵称”的用户但漏掉了一部分只写了nickname 忽略了 NULL 记录使用IFNULL(nickname, ) 邮箱为 NULL 的行在NOT IN中消失NOT IN 遇到 NULL 返回未知导致行被过滤加OR email IS NULL或用IFNULL(email,) NOT IN (...)动态 WHERE 条件没有生效判断参数为空时逻辑不统一Service 层使用统一的 isBlank 判断MyBatis 动态 SQL 抛 SQL 语法错误where使用不当或if拼出了多余的 AND检查where和if标签组合页面显示数据正常后台统计对不上COUNT(column) 与 COUNT(*) 混用明确统计口径区分 NULL 和空字符串出现 SQL 注入风险代码中直接拼接${}或 Java 字符串改为#{}或 PreparedStatement 参数化查询已删除数据被返回忘记携带 delete_flag 过滤条件所有业务查询统一加delete_flag0排查清单先看字段定义是允许 NULL 还是 NOT NULL。再确认数据里是否存在空字符串。然后确认业务逻辑中“空”的定义到底是什么。查看 SQL 有没有使用IS NULL、IFNULL、COALESCE。查看动态 SQL 构建代码是否安全是否使用参数化查询。在测试环境插入 NULL、空字符串、纯空格三条数据做验证。确认 delete_flag 等软删除字段是否参与了条件过滤。11. 网络安全视角下的最佳实践与工程建议11.1 数据库查询规范建议无论你是普通后端开发还是准备进入网络安全领域下面这些规范都可以直接用于日常项目统一空值判断函数。项目里可以定义一个公共 SQL 片段或统一工具类对于“是否为空”的判断使用统一规则避免每名开发人员写法不同。建表时明确字段约束。能设置为 NOT NULL 的字段尽量设置 NOT NULL并给默认值。例如delete_flag TINYINT NOT NULL DEFAULT 0这样能从根本上减少 NULL 和 0 混用的问题。字符串类型字段不要用 NULL 表示“未填写”。更推荐使用DEFAULT 并配合业务校验。当然这需要权衡因为 NULL 和空字符串在索引、存储上有差别团队要统一规范。动态拼接 WHERE 条件时必须使用安全参数绑定。在实际项目中禁止直接使用用户传入值拼接 SQL。重要查询带上 delete_flag。如果系统使用软删除一定要统一由框架层或 MyBatis 拦截器统一补充条件不能只靠开发人员手动记。11.2 SRC 漏洞挖掘中的自我约束在网络安全学习过程中需要不断建立“授权”意识。想练 SQL 注入、布尔盲注等技术应该在本地靶场、CTF 平台、SRC 平台授权的测试项目中进行。不要因为学会了IFNULL和 NULL 绕过思路就想去真实站点上验证那非常危险。SRC 平台通常有漏洞测试范围说明你只允许在指定域名和产品下测试。提交漏洞前要脱敏截图不能保存目标业务数据。这些既是职业道德也是法律底线。11.3 开发安全 Checklist最后给出一个适合开发的“安全自检清单”[ ] 代码中不存在用户输入直接拼接 SQL 的情况。[ ] MyBatis 只有#{}没有用户输入进入${}。[ ] 排序字段 / 表名等动态部分都有白名单校验。[ ] 所有查询都考虑了权限范围接口不能越权查看数据。[ ] 查询结果统一过滤已删除数据。[ ] 异常信息不会直接返回给前端避免暴露表结构和 SQL 片段。[ ] 日志中不记录明文密钥、完整手机号等敏感信息。[ ] 数据库账号遵循最小权限原则业务账号没有 DDL 或 DROP 权限。12. 小结这篇文章从 MySQL 条件查询的“空值判断”出发重点拆解了几个内容NULL与空字符串的区别。IS NULL/IS NOT NULL的正确用法。IFNULL、COALESCE、NULLIF、CHAR_LENGTH在空值处理中的应用。NOT IN遇到 NULL 的隐藏陷阱。动态拼 WHERE 条件的两种实现方式以及为什么安全漏洞常出现在这里。从网络安全角度理解 SQL 注入、软删除绕过和代码审计中的关键点。无论你当前处在“零基础入门”阶段还是已经开始接触 SEC 挖洞实战SQL 基础都会直接决定你对漏洞原理理解得深不深。很多人觉得 SQL 简单但其实真正能把 NULL、空字符串、条件拼接等细节处理好的人并不多。建议你打开本地 MySQL把上面每条 SQL 都执行一遍观察返回结果再习惯性思考一个安全相关问题“如果这条查询接口暴露在公网会不会有被绕过的可能”如果本文对你有帮助可以收藏备用。下一篇文章我们可以继续深入 MySQL 的排序、分组与聚合查询并结合网络安全常见的数据泄露场景讲解 ORDER BY 注入与 GROUP BY 逻辑。觉得有用的话欢迎在评论区留言我也会继续更新这个系列。
返回列表