4、危险的字符串函数:strcpy、strcat、sprintf、gets等函数的安全隐患

说实话,C语言里这几个字符串函数,是我在代码审查中见到最多的"定时炸弹"。每次看到新人用它们,我都会心头一紧。今天咱们就来聊聊,为什么这些函数被业界"拉黑",以及该用什么来替代。

4.1 这些函数到底危险在哪?

先看一个我亲身经历的案例。几年前我接手一个嵌入式项目,前任工程师用 strcpy 处理用户输入的用户名。结果呢?测试时只要输入超过16个字符,系统就崩溃。后来发现,缓冲区只有16字节,而 strcpy 根本不管这些,它只管往目标地址写,直到遇到 '\0' 为止。

这就是核心问题——不检查目标缓冲区大小。说白了,这些函数就像没有刹车的跑车,跑得快,但停不下来。

4.1.1 strcpy 的问题

char buf[10];
strcpy(buf, "这是一个非常长的字符串,远超10个字节"); // 缓冲区溢出!

这段代码会直接覆盖 buf 后面的内存。如果后面恰好是返回地址或函数指针,恭喜你,你给攻击者留了一扇门。

4.1.2 strcat 的问题

strcat 更坑。它不光不检查目标大小,还会先找到目标字符串的末尾,然后从那里开始拼接。如果目标缓冲区已经满了,它照样往上写。

char path[32] = "/home/user/";
strcat(path, "一个非常长的文件名,远超剩余空间"); // 溢出!

我在项目中遇到过,有人用 strcat 拼接路径,结果路径一长,直接覆盖了相邻的变量。调试了整整两天才找到原因。

4.1.3 sprintf 的问题

sprintf 是格式化输出的"重灾区"。它把格式化的结果写入缓冲区,但同样不检查长度。

char buf[32];
sprintf(buf, "用户ID: %d, 用户名: %s", id, username); // 如果username很长,就炸了

你想想看,如果 username 是用户输入的,攻击者完全可以构造一个超长字符串,触发缓冲区溢出。

4.1.4 gets 的问题——最危险,没有之一

gets 函数在 C11 标准中已经被正式移除。为什么?因为它从标准输入读取一行,完全不限制长度

char buf[100];
gets(buf); // 用户输入1000个字符?没问题,全给你写进去

嗯,这里要注意:gets 连目标缓冲区大小这个参数都没有,它根本不知道往哪写、写多少。这已经不是"不安全"了,这是"故意留后门"。

4.2 为什么不推荐使用?

总结一下,核心原因就三点:

  • 缓冲区溢出:写入超过目标缓冲区大小的数据,破坏相邻内存
  • 无法截断:即使知道数据太长,这些函数也不会帮你截断,而是继续写
  • 安全漏洞入口:缓冲区溢出是栈溢出攻击、ROP攻击等高级攻击的基础
⚠️ 重要警告: 不要相信任何来自外部的数据长度。用户输入、网络数据、文件内容,都可能比你预期的长。用这些危险函数,等于把系统的安全交给攻击者。

4.3 替代方案:安全版本的字符串函数

好消息是,C标准库提供了安全版本的替代函数。我个人习惯,在新项目中直接禁用 strcpystrcatsprintfgets,强制使用安全版本。

4.3.1 strncpy 替代 strcpy

char dst[16];
strncpy(dst, src, sizeof(dst) - 1);
dst[sizeof(dst) - 1] = '\0'; // 手动确保以'\0'结尾

注意:strncpy 不会自动添加 '\0',如果源字符串长度 >= 目标大小,目标字符串不会以 '\0' 结尾。所以一定要手动加。

💡 小技巧: 我习惯写一个宏来封装这个操作:
#define SAFE_STRCPY(dst, src) do { \
    strncpy(dst, src, sizeof(dst) - 1); \
    dst[sizeof(dst) - 1] = '\0'; \
} while(0)

4.3.2 strncat 替代 strcat

char path[32] = "/home/user/";
strncat(path, filename, sizeof(path) - strlen(path) - 1);

strncat 会检查剩余空间,最多追加指定数量的字符。它比 strcat 安全得多。

4.3.3 snprintf 替代 sprintf

char buf[32];
snprintf(buf, sizeof(buf), "用户ID: %d, 用户名: %s", id, username);

snprintf 会保证写入不超过 sizeof(buf) 个字符,并且自动添加 '\0'。这是我最推荐的格式化输出方式。

4.3.4 fgets 替代 gets

char buf[100];
if (fgets(buf, sizeof(buf), stdin)) {
    // 去掉末尾的换行符
    buf[strcspn(buf, "\n")] = '\0';
}

fgets 会限制读取长度,不会溢出缓冲区。而且它还能处理文件流,比 gets 灵活得多。

4.4 知识体系图

下面这张图展示了本章的核心逻辑:危险函数与安全替代方案的对应关系。

字符串函数安全替代方案 ❌ 危险函数 strcpy strcat sprintf gets ✅ 安全替代 strncpy strncat snprintf fgets 核心原则:始终指定缓冲区大小,确保不会溢出

4.5 避坑指南

我曾经在代码审查中看到过这样的"骚操作":

strncpy(dst, src, strlen(src)); // 这跟strcpy有什么区别?!

嗯,这完全失去了 strncpy 的意义。正确的做法是用 sizeof(dst) 作为长度参数。

还有一次,我看到有人用 snprintf 但传错了大小:

snprintf(buf, strlen(buf), "数据: %s", data); // 应该用sizeof(buf)!

这会导致 snprintf 认为缓冲区只有当前字符串长度那么大,照样溢出。

📌 核心原则:
  • 永远用 sizeof() 获取缓冲区大小,不要硬编码
  • 永远手动确保字符串以 '\0' 结尾(尤其用 strncpy 时)
  • 永远不要用 gets,哪怕是在玩具代码里
  • 格式化输出优先用 snprintf,而不是 sprintf

说白了,安全编程就是养成好习惯。这些替代函数用起来也就多打几个字符,但能避免90%的缓冲区溢出问题。何乐而不为呢?

💡 个人建议: 在团队项目中,可以在代码规范里明确禁止使用这些危险函数,并在 CI 流程中加入静态检查工具(如 cppcheckflawfinder),自动拦截违规代码。这样从流程上杜绝隐患。

公众号:蓝海资料掘金营,微信deep3321