CodeQL 文档

非固定格式字符串

ID: cpp/non-constant-format
Kind: path-problem
Security severity: 9.3
Severity: recommendation
Precision: high
Tags:
   - maintainability
   - correctness
   - security
   - external/cwe/cwe-134
Query suites:
   - cpp-security-extended.qls
   - cpp-security-and-quality.qls

点击查看 CodeQL 代码库中的查询

printf 函数、相关的函数(如 sprintffprintf),以及基于 vprintf 构建的其他函数都接受一个格式字符串作为其参数之一。当这些格式字符串是字面常量时,程序员(和静态分析工具)很容易验证格式字符串中的格式说明符(例如 %s%02x)是否与函数调用的尾随参数兼容。当这些格式字符串不是字面常量时,维护程序会更加困难:程序员(和静态分析工具)必须执行非局部数据流分析以推断格式字符串参数可能取什么值。

建议

如果作为格式字符串传递的参数意图是一个普通字符串而不是一个格式字符串,则将 %s 作为格式字符串传递,并将原始参数作为唯一的尾随参数传递。

如果作为格式字符串传递的参数是封闭函数的参数,则考虑重新设计封闭函数的 API,使其更不易出错。

示例

以下程序旨在回显其命令行参数

#include <stdio.h>
int main(int argc, char** argv) {
  for(int i = 1; i < argc; ++i) {
    printf(argv[i]);
  }
}

上述程序在大多数情况下按预期运行,但在其命令行参数之一包含百分号字符时会中断。在这种情况下,程序的行为是未定义的:它可能回显垃圾数据,可能崩溃,或者可能让恶意攻击者获得 root 访问权限。解决问题的一种方法是使用常量 %s 格式字符串,如以下程序所示

#include <stdio.h>
int main(int argc, char** argv) {
  for(int i = 1; i < argc; ++i) {
    printf("%s", argv[i]);
  }
}

示例

以下程序定义了一个 log_with_timestamp 函数

void log_with_timestamp(const char* message) {
  struct tm now;
  time(&now);
  printf("[%s] ", asctime(now));
  printf(message);
}

int main(int argc, char** argv) {
  log_with_timestamp("Application is starting...\n");
  /* ... */
  log_with_timestamp("Application is closing...\n");
  return 0;
}

在可见的代码中,读者可以验证 log_with_timestamp 永远不会被调用,其中日志消息包含百分号字符,但即使所有当前调用都是正确的,这也为确保新引入的调用不包含百分号字符带来了持续的维护负担。与前面的示例一样,一种解决方案是将日志消息作为函数调用的尾随参数

void log_with_timestamp(const char* message) {
  struct tm now;
  time(&now);
  printf("[%s] %s", asctime(now), message);
}

int main(int argc, char** argv) {
  log_with_timestamp("Application is starting...\n");
  /* ... */
  log_with_timestamp("Application is closing...\n");
  return 0;
}

另一种解决方案是允许 log_with_timestamp 接受格式参数

void log_with_timestamp(const char* message, ...) {
  va_list args;
  va_start(args, message);
  struct tm now;
  time(&now);
  printf("[%s] ", asctime(now));
  vprintf(message, args);
  va_end(args);
}

int main(int argc, char** argv) {
  log_with_timestamp("%s is starting...\n", argv[0]);
  /* ... */
  log_with_timestamp("%s is closing...\n", argv[0]);
  return 0;
}

在此公式中,printf 的非固定格式字符串已被替换为 vprintf 的非固定格式字符串。分析将不再将 log_with_timestamp 的主体视为问题,而是将检查对 log_with_timestamp 的每次调用是否传递了固定格式字符串。

参考文献

  • ©GitHub, Inc.
  • 条款
  • 隐私