sprintf/snprintf哪个更安全?

65

我想知道这两种选项中哪一个更安全:

#define MAXLEN 255
char buff[MAXLEN + 1]
  1. sprintf(buff, "%.*s", MAXLEN, name)

  2. snprintf(buff, MAXLEN, "%s", name)

我的理解是这两者是相同的。请给予建议。


1
将#2更改为MAXLEN+1,它们在所有情况下写入buff中的内容将是相同的(如果strlen(name)>255,则返回值将不同)。 - Chris Dodd
8个回答

50
你提供的这两个表达式是不等价的sprintf 不接受指定要写入的最大字节数的参数;它只需要一个目标缓冲区、一个格式化字符串和一些参数。因此,它可能会写入比你的缓冲区还要多的字节,并在这样做时写入任意代码。 %.*s 不是一个令人满意的解决方案,因为:
  1. 当格式说明符引用长度时,它是指等同于strlen的长度;这是字符串中字符数的度量,而不是其在内存中的长度(即它不计算空终止符)。
  2. 格式字符串的任何更改(例如添加换行符)都将改变sprintf版本相对于缓冲区溢出的行为。使用snprintf,无论格式字符串或输入类型如何改变,都设置了一个固定的、明确的最大值。

实际上,关于1,我犯了错误 - 它确实指定了最大字符数(但不包括计算\0)。我已相应地编辑了我的答案。 - Eli Iser
谢谢 - 我记得那个格式说明符有些问题,所以我认为你是对的 :-P - azernik
2
嗯,这完全取决于OP的问题涉及哪种类型的安全性。在正式情况下,如果sprintf被正确使用,则在此特定情况下与snprintf一样安全。您在本答案中所说的是对懒惰/不称职程序员的保护不足。我不知道OP是否正在问及安全方面的这一方面。 - AnT stands with Russia
1
我假设字符串 name 是由不受信任的用户提供的;这是唯一可能存在安全问题的情况,而且在这种情况下,它是相当严重的。使用一个非常糟糕的 sprintf 格式字符串,恶意用户可以仔细地构造他们的输入,覆盖堆栈并执行任意代码;而使用这个格式字符串,恶意用户只能用零(空终止符)覆盖堆栈上的一个字节,这可能是一种相当有效的 DOS 攻击。 - azernik
1
然而,一个好的格式化字符串提供了与snprintf相同级别的保护(再次特别应用于字符串输入)。 - AnT stands with Russia
8
它确实可以,但可验证性要低得多 - 很难一眼看出sprintf调用会覆盖多少内存上限。 - azernik

25

最好、最灵活的方法是使用snprintf

size_t nbytes = snprintf(NULL, 0, "%s", name) + 1; /* +1 for the '\0' */
char *str = malloc(nbytes);
snprintf(str, nbytes, "%s", name);
在C99中,snprintf函数返回写入到字符串中的字节数(不包括'\0')。如果字节数不足以容纳格式化后的内容,则snprintf返回扩展所需的字节数(仍然不包括'\0')。通过传递长度为0的字符串给snprintf,您可以提前了解扩展后字符串的长度,并使用它来分配必要的内存。

2
是的,这是最好的方法,我不知道为什么这个答案没有被投到顶部。用户通常不知道他们可以将NULL作为第一个参数。 - user26742873
https://man7.org/linux/man-pages/man3/asprintf.3.html 更进一步,只为您分配它。GNU扩展。 - erik258
从性能上来看,这个解决方案的执行时间大约是原来的两倍吗? - Daniel Chin
从性能上来看,这个解决方案的执行时间大约是原来的两倍吗? - undefined

15

对于问题中的简单示例,这两个调用之间可能并没有太大的安全差异。然而,在一般情况下,snprintf() 可能更加安全。一旦您有了一个更复杂的格式字符串,并且其中包含多个转换说明符,就很难(或者几乎不可能)确保您在不同的转换说明符中准确地考虑了缓冲区长度,特别是因为前面的转换说明符不一定会产生固定数量的输出字符。

所以,我建议坚持使用 snprintf()

snprintf() 的另一个小优点(虽然与安全无关)是它会告诉您需要多大的缓冲区。

最后要注意的一点是,您应该在 snprintf() 调用中指定实际的缓冲区大小 - 它会为您处理空终止符:

snprintf(buff, sizeof(buff), "%s", name);

2
我认为值得明确说明的是,在您的最后一行代码中,buff 必须是堆栈上的数组。如果您有一个 char *,它将无法工作。(嗯,它可能会,但至少不总是有效。)是的,它在 OP 的情况下可以工作,但原因对于使用此作为参考的新人来说是微妙的。 - anon
尼克,你能提供一下“必须在堆栈上是一个数组”的依据吗? - Cognitive Hazard

12

在读到这段话之前,我会说snprintf()更好用。

https://buildsecurityin.us-cert.gov/bsi/articles/knowledge/coding/838-BSI.html

简短地说,snprintf() 不具可移植性,其行为在系统之间可能会发生变化。最严重的问题是当 snprintf() 仅通过调用 sprintf() 实现时可能会发生的。你可能认为它可以保护你免受缓冲区溢出的影响,从而放松警惕,但实际上并非如此。

因此,我现在仍然认为使用snprintf()更安全,但在使用时也要谨慎小心。


snprintf 在 ISO C 99 中被指定,这比本答案早了14年,比今天早了24年。引用的2007年参考文献正确地说明了 snprintf 不在 C90 中,并且有不同的变体。但是...那又怎样? - Kaz

6

这两者之间有一个重要的区别--snprintf调用将扫描name参数直到末尾(终止NUL),以确定正确的返回值。另一方面,sprintf调用将从name中读取最多255个字符。

所以如果name是指向非NUL终止缓冲区且至少有255个字符的指针,则snprintf调用可能会超过缓冲区的末尾并触发未定义行为(例如崩溃),而sprintf版本则不会。


3
你的sprintf语句是正确的,但我不会自信地将其用于安全目的(例如缺少一个神秘字符,你就没有保护了),因为有snprintf可以用于任何格式...哦等等,snprintf不在ANSI C中,它只在C99中存在。这可能是倾向于使用其他语句的(弱)原因。
好吧。你也可以使用strncpy,不是吗?
例如:
  char buffer[MAX_LENGTH+1];
  buffer[MAX_LENGTH]=0;             // just be safe in case name is too long
  strncpy(buffer,MAX_LENGTH,name);  // strncpy will never overwrite last byte

你确定 %.*s 在 ANSI C 中可用吗?我尝试查找规范,但只找到了一个(不可靠的)参考,没有指定 .* - Eli Iser
@Eli:指定精度(如此处)或字段宽度作为星号自 ANSI C 标准(1989/1990)以来就存在了。snprintf() 是在 C99 标准中添加的。 - Michael Burr
@Michael - 对于字符串(%s)也适用吗?这个好知道,谢谢。 - Eli Iser
2
不,strncpy不是他想要的,请在此处搜索strlcpy以了解原因。例如:https://dev59.com/l3I95IYBdhLWcg3w3iDG#2115015 - quinmars
@quinmars:AndreyT在这里给我指出了一个很长的胡言乱语...如果我试着总结一下,strncpy的问题在于如果源字符串太长,它可能不会放置一个尾随的'\0',对吧?这听起来对我来说很容易克服(请参见我帖子中的更新代码)。现在,如果缺少额外的"buffer[N]=0",那么可能会感到"不安全"。或者还有其他我错过的东西吗? - PypeBros

0
所以这是在搜索sprintf vs snprintf时出现的第一个与堆栈溢出相关的问题。所以我觉得这是最好的地方来添加这个答案。
看起来在OSX 13.3上现在不能使用sprintf,因为我收到了这些警告。
'sprintf'已被弃用:此函数仅供兼容性使用。由于sprintf(3)设计中存在的安全问题,强烈建议您改用snprintf(3)。[-Werror,-Wdeprecated-declarations]
所以我遍历了它们并将它们全部转换为snprintf。
snprintf(char * restrict str, size_t size, const char * restrict format, ...);

这个问题相当简单,使用snprintf比sprintf更安全。

-1

两种方法都可以得到你想要的结果,但是snprintf更通用,并且无论给定的格式字符串如何,都会保护你的字符串免受溢出的影响。

此外,因为snprintf(或者说sprintf)会添加一个最终的\0,所以你应该将字符串缓冲区增加一个字节,即char buff[MAXLEN + 1]


2
事实并非如此。从snprintf文档中可以看到:“函数snprintf()和vsnprintf()最多写入size个字节(包括终止的空字符('\0'))到str中。” - Chriszuma

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接