
静态分析SAST应用安全漏洞扫描代码质量【免费下载链接】codeqlCodeQL: the libraries and queries that power security researchers around the world, as well as code scanning in GitHub Advanced Security项目地址https://gitcode.com/gh_mirrors/co/codeql点击查看免费下载本篇指南基于 CodeQL 仓库中 C/C 查询包的 0.4.0 版本发布说明cpp/ql/src/change-notes/released/0.4.0.md展开聚焦两项核心变更新增的cpp/missing-check-scanf查询如何通过两阶段数据流分析发现scanf输出变量在缺少守卫的情况下被读取以及 Cleartext明文敏感数据系列查询的现代化移植如何减少误报并提升正确性。读完本文你将掌握scanf返回值检查的正确姿势、CodeQL 污点追踪配置的编写思路并能依据源码与测试用例验证这些查询的实际行为。版本背景与变更总览0.4.0.md是cpp/ql/src/change-notes/released/目录下按语义化版本组织的发布说明之一同一条目也收录在 cpp/ql/src/CHANGELOG.md 中。0.4.0 版本的变更可以归纳为三类新增查询medium 精度的cpp/missing-check-scanf检测scanf输出变量在未检查返回值以确认其确实被写入的情况下就被使用分析改进将cpp/cleartext-storage-buffer缓冲区明文存储查询的现代化成果移植到cpp/cleartext-storage-file文件明文存储、cpp/cleartext-transmission明文传输与cpp/cleartext-storage-databaseSQLite 数据库明文存储三个查询预期带来更正确的结果和更少的误报消息统一大量查询的告警消息被改写使其与其它语言保持一致。新增查询 cpp/missing-check-scanf为什么非零检查不够scanf家族函数在输入/输出失败IO 失败或格式不匹配时返回EOF一个负值成功时返回成功匹配并赋值的输入项数。因此把返回值当作布尔值做非零即成功的检查并不安全例如if (scanf(%d, x))在读取失败返回EOF时同样进入分支EOF为负值非零x却未被写入后续读取x就是未定义行为。这一问题的正确修复方式参见查询帮助文档 MissingCheckScanf.qhelp 中的 recommendation所有对scanf输出参数的使用都应位于能证明对应scanf调用确实读取了全部所需输入项的分支内通常通过将返回值与一个数值常量比较实现。查询元数据与问题定位查询主体 MissingCheckScanf.ql 的元数据声明如下元数据项取值含义kindpath-problem结果以源到汇路径问题形式报告可展示数据流路径problem.severitywarning告警级别security-severity7.5安全严重度评分precisionmedium中等精度见下文精度说明idcpp/missing-check-scanf查询唯一标识tagssecurity、correctness、external/cwe/cwe-252、external/cwe/cwe-253关联 CWE-252未检查返回值与 CWE-253检查了错误的返回值查询帮助文档特别说明该查询是medium 精度因为当前实现采取了较严格的立场——即使输出变量已经被初始化只要其使用未被正确守卫仍会被标记。示例代码 MissingCheckScanf.cpp 展示了这一立场。两阶段数据流先未初始化到调用再调用到使用从 MissingCheckScanf.ql 的源码结构看查询由两个独立的数据流配置串联而成这是整个实现最值得学习的设计阶段一UninitializedToScanfConfig未初始化数据流向scanf输出参数配置的源isSource由isUninitialized谓词定义MissingCheckScanf.ql#L29-L32predicate isUninitialized(Node n) { exists(n.asUninitialized()) or n.asIndirectExpr(1) instanceof AllocationExpr }即栈上未初始化的变量或新建可视为未初始化的堆分配。汇isSink是scanf调用输出参数位置的间接表达式。该配置特意关闭了字段流accessPathLimit() 0并要求源与汇位于同一可调用体内FeatureEqualSourceSinkCallContext。源码注释道明了设计意图这一阶段刻意保持简单用于排除如下场景——int x 0; scanf(%d, x); use(x);x已被初始化因此即使不检查scanf返回值也不构成读取未写入变量的安全问题无需报告。阶段二ScanfToUseConfig从scanf输出参数到使用点源是经过scanf调用定义asDefiningArgument的输出参数节点汇是任何表达式节点但排除释放表达式如free、delete的被释放参数MissingCheckScanf.ql#L88-L95避免把释放scanf输出变量误报为读取。配置还设置了两个isBarrierOut条件MissingCheckScanf.ql#L106-L113汇节点本身禁止继续流出——用于减少结果重复节点作为间接实参传给函数时停止追踪——因为该变量可能被函数修改之后的读取是安全的。这两个配置串联后flowPath谓词MissingCheckScanf.ql#L122-L129完整刻画了未初始化变量 →scanf输出参数 → 被读取的路径。最小守卫常数精确到每个输出参数scanf的返回值等于成功写入的项数。对第index个0 起输出参数而言要确保它确实被写入返回值至少应为index 1。但有一个特例%n转换说明符只写入参数、不消费输入不增加scanf的返回值。因此getMinimumGuardConstantMissingCheckScanf.ql#L135-L144这样计算最小守卫常量result index 1 - count(ScanfFormatLiteral f, int n | n index and f.getUse() call and f.getConversionChar(n) n )即索引 1 减去前index个转换说明符中%n的个数。这一细节直接体现在测试基线中例如 MissingCheckScanf.expected 中scanf的第二个输出参数被要求returns at least 2sscanf的第三个输出参数被要求returns at least 3。守卫判定识别足够强的检查hasNonGuardedAccessMissingCheckScanf.ql#L150-L170负责判断某次读取是否被够格的守卫保护。它利用 IR 守卫条件GuardCondition与全局值编号GlobalValueNumbering检查在读取所在基本块上是否存在下列任一保证guard.ensuresEq(call, k)且k minGuard即call k而k不小于最小守卫值guard.ensuresLt(call, k, false)且k minGuard即call kk不小于最小守卫值。换言之r 2、r 2、r ! 1这类能推出返回值至少为 2的守卫都会被识别为正确守卫而if (r)这种只检查非零的写法不会被当作有效守卫。与 IncorrectCheckScanf 的分工查询还通过 ScanfChecks.qll 中的incorrectlyCheckedScanf谓词排除一部分调用如果scanf返回值只出现在布尔上下文if (scanf(...))且没有被证明检查过EOF则该调用归错误检查一类由同目录的cpp/incorrect-check-scanf查询覆盖cpp/missing-check-scanf不再重复报告MissingCheckScanf.ql#L71-L75。checkedForEof谓词ScanfChecks.qll#L34-L49识别call EOF、call 0或call 非负数这类检查其中EOF的值通过宏调用动态获取典型为-1但并非所有平台都保证。值得注意的是ScanfChecks.qll#L13-L16 为 Linux 内核代码做了特判当文件包含_LINUX_KERNEL_SPRINTF_H_或_LINUX_KERNEL_H头文件守卫宏时认为内核中的scanf不会返回EOF从而if (scanf(...))在内核代码中是合法的。覆盖的函数族scanf-like 函数的建模集中在公共库 cpp/ql/lib/semmle/code/cpp/commons/Scanf.qll 中。抽象类ScanfFunction之下有四个具体子类Scanf.qll#L42-L119类覆盖函数格式串参数索引Scanfscanf、wscanf、scanf_s、wscanf_s及_scanf_l、_wscanf_l、_scanf_s_l、_wscanf_s_l0Fscanffscanf、fwscanf、fscanf_s、fwscanf_s及_l变体1Sscanfsscanf、swscanf、sscanf_s、swscanf_s及_l变体1Snscanf_snscanf、_snwscanf及_l变体2ScanfFunctionCall类Scanf.qll#L132-L209把变长参数中的输出参数抽象为getOutputArgument(n)对于_s安全变体它还会跳过紧随字符串缓冲区参数的 size 参数isSizeArgument确保输出参数编号正确。ScanfFormatLiteral类Scanf.qll#L214-L333则负责解析格式串通过正则表达式拆解每个转换说明符的宽度、长度修饰符与转换字符并在解析前将%%与%*替换为占位符避免误当作转换说明符。这些能力正是getMinimumGuardConstant能精确计算%n个数的底层基础。示例与测试验证MissingCheckScanf.cpp 是查询帮助文档引用的官方示例逐行展示了正确与错误的守卫{ int i, j, r; r scanf(%d %d, i, j); use(i); // BAD: i is not guarded if (r 1) { use(i); // GOOD: i is guarded correctly use(j); // BAD: j is guarded incorrectly } if (r ! 2) return; use(j); // GOOD: j is guarded correctly }r 1只能证明第一个输出参数i被写入返回值为 1 时第二个参数j未写入因此块内读取j仍被判为 BAD只有r ! 2等价于r 2才继续才能正确保护j。该查询的回归测试位于 cpp/ql/test/query-tests/Critical/MissingCheckScanf/由 MissingCheckScanf.qlref 指向查询。基线文件记录了数十条scanf/fscanf/sscanf/_scanf_l的edges、nodes与#select结果既覆盖%n计数特例也覆盖**buf解引用读取等场景可以作为理解查询行为的活教材。该查询最初由社区开发者 ihsinme 以实验性查询形式贡献实验性版本文档仍保留在 cpp/ql/src/experimental/Security/CWE/CWE-754/ImproperCheckReturnValueScanf.qhelp0.4.0 将其打磨为正式查询。Cleartext 明文敏感数据查询的现代化现代化的起点buffer 查询Cleartext 系列查询关注敏感信息密码、账户密钥等未加密地被存储或传输。现代化的源头发端于 buffer 查询cpp/cleartext-storage-buffer在 0.3.3 版本已先行改进以产生更少的误报见 CHANGELOG.md#L652-L6540.4.0 将这套现代化方案移植到 file、transmission、database 三个查询。从 CleartextBufferWrite.ql 的源码结构看现代化后的模式包含四个要素共享的敏感表达式识别通过import semmle.code.cpp.security.SensitiveExprs识别密码、密钥等敏感数据源汇则是写入敏感表达式的BufferWrite操作SensitiveBufferWrite类污点追踪配置ToBufferConfigCleartextBufferWrite.ql#L42-L60将FlowSource用户输入源到敏感 buffer 写入的路径串联起来整数屏障isBarrier把所有整型表达式节点设为屏障CleartextBufferWrite.ql#L45-L47防止污点沿memcpy、fprintf的长度/格式等整型参数扩散这是减少误报的关键手段增量模式适配observeDiffInformedIncrementalMode谓词声明查询支持差分感知的增量评估便于大规模数据库上的重复分析。移植到 file / transmission / database三个接收移植的查询分别位于CleartextFileWrite.qlcpp/cleartext-storage-file高精度CWE-260/313配置FromSensitiveConfig追踪敏感表达式到FileWrite汇CleartextTransmission.qlcpp/cleartext-transmission通过Send/Recv类CleartextTransmission.ql#L57-L103基于RemoteFlowSinkFunction/RemoteFlowSourceFunction建模网络发送/接收函数NetworkSendRecv会排除套接字为常量大概率是 stdin/stdout 等通道的调用且接受write这类既可能写文件也可能写网络的函数并保证结果只报告一次CleartextSqliteDatabase.qlcpp/cleartext-storage-database通过SqliteFunctionCall抽象类建模sqlite3_prepare%、sqlite3_exec等调用CWE-313。三个查询与 buffer 查询共享同一套SensitiveExprs识别逻辑与污点追踪框架CleartextTransmission.ql还额外构建了加密净化模型Encrypted类与ToEncryptionFlow/FromEncryptionFlowCleartextTransmission.ql#L186-L303数据一旦流入加密操作就不再被视为明文敏感数据从而避免对先加密再传输的正确代码误报。这些查询共用一份帮助文档模板 CleartextStorage.inc.qhelp其中的推荐做法是敏感信息在写入文件、传输到网络之前必须加密仅在确实需要明文使用的位置才解密。三者的测试基线如 CleartextFileWrite.expected显示告警消息已统一为带具体目标的格式例如This write into file log may contain unencrypted data from $.告警消息的跨语言统一0.4.0 的另一项变更延续至 0.4.1见 CHANGELOG.md#L633-L635是把大量查询的告警消息改写为与其他语言一致消息现在包含具体的目标buffer、file、传输通道与数据来源类型例如CleartextBufferWrite.ql的 select 语句CleartextBufferWrite.ql#L73-L75select w, sourceNode, sinkNode, This write into buffer w.getDest().toString() may contain unencrypted data from $., source, user input ( source.getSourceType() )统一后的消息更利于跨语言C/C、C#、Java、Python 等结果聚合与团队告警处理流程的标准化。在自己的代码库中使用这些查询上述查询均为cpp查询包正式发布的查询位于 cpp/ql/src/Critical/cpp/missing-check-scanf与 cpp/ql/src/Security/CWE/cleartext 系列下。使用方式与 CodeQL 标准流程一致用codeql database create为 C/C 项目构建 CodeQL 数据库用codeql database analyze或codeql query run执行上述查询或运行cpp查询包中的对应查询套件在结果中优先关注path-problem提供的完整数据流路径逐跳核实敏感数据确实未经检查或未经加密。使用时有两点提示其一cpp/missing-check-scanf为 medium 精度实现上对已初始化但未守卫的输出变量同样报告排查时需结合上下文判断其二cleartext 系列查询依赖SensitiveExprs库对敏感数据源的识别若你的代码使用了自定义的敏感数据结构可结合该库的扩展机制补充建模。若想深入理解查询行为MissingCheckScanf.expected 与 CleartextFileWrite.expected 是两份值得通读的基线样例。赞分享静态分析SAST应用安全漏洞扫描代码质量【免费下载链接】codeqlCodeQL: the libraries and queries that power security researchers around the world, as well as code scanning in GitHub Advanced Security项目地址https://gitcode.com/gh_mirrors/co/codeql点击查看免费下载相关推荐CodeQL C 0.5.0 改进解析codeql[query-id] 告警抑制注释与 scanf 返回值检查优化CodeQL C 0.5.0 改进解析 codeql query id 告警抑制注释与 scanf 返回值检查优化 本文基于 CodeQL 仓库中 cpp静态分析SAST应用安全漏洞扫描代码质量CodeQL C 查询演进WrongTypeFormatArguments 如何排除隐式声明函数的返回值CodeQL C 查询演进WrongTypeFormatArguments 如何排除隐式声明函数的返回值 本篇技术指南聚焦 CodeQL C 分析器在静态分析SAST应用安全漏洞扫描代码质量CodeQL C/C 1.18 分析能力升级新查询、查询改进与 QL 库变更深度解析CodeQL C/C 1.18 分析能力升级新查询、查询改进与 QL 库变更深度解析 本文对应 CodeQL 仓库 change notes/1.18/a静态分析SAST应用安全漏洞扫描代码质量创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考