为什么strlcpy和strlcat被认为是不安全的?

87
我理解strlcpystrlcat是为了替代不安全的strncpystrncat而设计的。然而,一些人仍然认为它们存在安全问题,并只是引起了不同类型的问题
有人能举个例子来说明使用strlcpystrlcat(即总是在字符串末尾添加空字符的函数)如何会导致安全问题吗?
Ulrich Drepper和James Antill表示这是真实存在的,但从未提供过例子或澄清此观点。
8个回答

116

首先,strlcpy从未被设计成strncpy的安全版本(而strncpy也从未被设计为strcpy的安全版本)。这两个函数完全没有关联。 strncpy是一个与C字符串(即以null结尾的字符串)毫无关系的函数。它的名称中带有str...前缀只是历史上的失误。 strncpy的历史和目的是众所周知并有充分的记录。它是一个为处理某些历史版本的Unix文件系统中使用的“固定宽度”字符串(而非C字符串)而创建的函数。今天一些程序员会因其名称而感到困惑,并假设strncpy应该作为有限长度的C字符串复制函数(strcpy的“安全”姊妹函数),但实际上这完全是胡说八道,会导致糟糕的编程实践。 C标准库在其当前形式中根本没有任何有限长度的C字符串复制函数。这就是strlcpy的用处所在。 strlcpy确实是一个真正的有限长度复制函数,用于处理C字符串。 strlcpy正确地执行了所有有限长度复制函数应该执行的操作。唯一可以批评它的是,遗憾的是它不是标准函数。

其次,另一方面,strncat的确是一个用于处理C字符串并执行有限长度连接的函数(它是strcat的“安全”兄弟)。为了正确使用此函数,程序员必须特别小心,因为此函数接受的大小参数实际上并不是接收结果的缓冲区的大小,而是其剩余部分的大小(终止字符也隐含计算在内)。这可能会令人困惑,因为为了将该大小与缓冲区的大小联系起来,程序员必须记住执行一些额外的计算,这通常被用来批评strncatstrlcat解决了这些问题,改变了接口,使得不需要进行额外的计算(至少在调用代码中不需要)。再次强调,我认为唯一可以批评的基础是该函数不是标准函数。此外,strcat组的函数由于基于重新扫描的字符串连接的有限可用性,在专业代码中很少出现。
至于这些函数如何导致安全问题...他们根本不能。他们不能比C语言本身更容易“导致安全问题”。你看,有一段时间,社会上普遍认为C++语言必须朝着发展成某种奇怪的Java口味的方向发展。这种情绪有时也会波及到C语言的领域,导致对C语言特性和C标准库特性的无知和迫切批评。尽管我真心希望事情不会变得太糟糕,但我怀疑我们在这种情况下也可能面临类似的问题。

10
我不完全同意。如果strlcpystrlcat达到了目标缓冲区大小的限制,报告某种错误状态会很好。虽然你可以检查返回的长度来测试此条件,但这不是很明显。但我认为这只是一个小的批评。声称它们鼓励使用C字符串,因此它们是不好的的观点是愚蠢的。 - Omnifarious
7
这些函数如何导致安全问题。就我所知,问题在于有些 C 函数比其他函数更难正确使用。一些人错误地认为存在一个特定的难度门槛,低于此门槛的函数是“安全”的,高于此门槛的函数则是“不安全”的。这些人通常认为 strcpy 的难度超过了门槛,因此是“不安全”的;他们更喜欢的字符串复制函数(无论是 strlcpystrcpy_s 还是 strncpy)的难度低于门槛,因此是“安全”的。 - Steve Jessop
33
不喜欢strlcpy/strlcat的原因有很多,但你没有说明其中任何一个。关于C++和Java的讨论是无关的。这个答案对于问题实际提出的主题没有帮助。 - John Ripley
26
@John Ripley:首先,我没有“陈述任何一个”,因为我不知道为什么会不喜欢strlcpy/strlcat。有人可能“不喜欢”零终止字符串的一般概念,但这不是问题的重点。如果你知道“很多不喜欢strlcpy/strlcat的理由”,你应该自己编写答案,而不是期望我能读懂别人的想法。 - AnT stands with Russia
10
@John Ripley:其次,该问题特别涉及strlcpy/strlcat存在的一些所谓“安全问题”。虽然我相信我理解这是关于什么的,但我个人拒绝承认这是传统C语言领域内的“安全问题”,因为我知道的是如此。这正是我在我的回答中所陈述的。 - AnT stands with Russia
显示剩余4条评论

32

Ulrich批评的基础是,程序没有检测到的字符串截断可能会通过不正确的逻辑导致安全问题。因此,为了安全起见,您需要检查截断。对于字符串连接,要执行这个检查意味着你需要进行类似以下的检查:

if (destlen + sourcelen > dest_maxlen)
{
    /* Bug out */
}

现在,strlcat 实际上确实进行了这个检查,如果程序员记得检查结果,那么你是可以安全使用它的:

if (strlcat(dest, source, dest_bufferlen) >= dest_bufferlen)
{
    /* Bug out */
}

Ulrich的观点是,由于您必须拥有destlensourcelen(或重新计算它们,这正是strlcat有效地执行的操作),因此您可能还是更好地使用更高效的memcpy

if (destlen + sourcelen > dest_maxlen)
{
    goto error_out;
}
memcpy(dest + destlen, source, sourcelen + 1);
destlen += sourcelen;

在上述代码中,dest_maxlen 是能够存储在 dest 中的字符串的最大长度 - 比 dest 缓冲区的大小小一。 dest_bufferlendest 缓冲区 的完整大小。


15
Drepper的代码可读性很差。使用strlcpy(或任何str函数),我可以直接知道我正在复制一个以0结尾的C字符串。而使用memcpy时,它可以是任何类型的内存,并且在尝试理解代码时需要进行额外的检查。我曾经需要调试一个使用了memcpy完成所有操作的遗留应用程序,这真是一项非常棘手的任务。但是,将其移植到专门的字符串函数后,代码阅读起来变得更容易(速度也更快,因为许多不必要的strlen语句都可以删除)。 - Patrick Schlüter
在最后一个例子中,为什么不使用strcpy而是使用memcpy - domen
3
由于要复制的大小已知,因此使用memcpy()足以实现(并且潜在地比strcpy()更有效率)。 - caf
1
好吧,在字符串操作中使用它很令人困惑。就我所知,效率取决于实现方式,而不是标准化的。 - domen
8
@domen: memcpy() 一个字符串操作 - 毕竟它在 <string.h> 中声明了。 - caf
2
@domen 我同意可能会有混淆的潜在问题,但事实上,处理C字符串基本上就是处理原始内存。可以说,如果人们停止将C视为具有“字符串”(与任何其他连续内存块不同)的语言,我们都会更好。 - mtraceur

22
当人们说“strcpy()是危险的,请使用strncpy()代替”(或类似strcat()等语句,但我将以strcpy()为重点),他们的意思是strcpy()中没有边界检查。因此,过长的字符串会导致缓冲区溢出。他们是正确的。在这种情况下使用strncpy()将防止缓冲区溢出。
我认为strncpy()并不能真正解决问题:优秀的程序员可以很容易避免这个问题。
作为一个C程序员,在你尝试复制字符串之前,必须知道目标大小。这就是strncpy()strlcpy()的最后一个参数所假设的:你向它们提供了该大小。你也可以在复制字符串之前知道源大小。然后,如果目标不够大,就不要调用strcpy()。要么重新分配缓冲区,要么采取其他措施。
为什么我不喜欢strncpy()
  • strncpy()在大多数情况下都是一个糟糕的解决方案:你的字符串将被截断而没有任何提示- 我宁愿编写额外的代码自己解决这个问题,然后采取我想要采取的行动,而不是让某些函数为我决定该做什么。
  • strncpy()非常低效。它会写入目标缓冲区中的每个字节。你不需要在目标末尾有成千上万个'\0'
  • 如果目标不够大,它不会写入终止的'\0'。因此,你必须自己完成这一任务。做这件事的复杂性不值得麻烦。
现在,我们来讨论一下strlcpy()。与strncpy()相比,它进行了改进,但我不确定strl*的特定行为是否值得存在:它们过于特定。您仍然需要知道目标大小。它比strncpy()更有效,因为它不一定会写入目标中的每个字节。但它解决了一个可以通过执行以下操作来解决的问题:*((char*)mempcpy(dst, src, n)) = 0;
我认为没有人会说strlcpy()strlcat()会导致安全问题,他们(和我)所说的是,它们可能会导致错误,例如当您期望完整字符串被写入而不是其中的一部分时。
主要问题在于:要复制多少字节?程序员必须知道这一点,如果他不知道,strncpy()strlcpy()也无济于事。 strlcpy()strlcat()都不是标准的ISO C或POSIX。因此,在便携式程序中使用它们是不可能的。事实上,strlcat()有两个不同的变体:Solaris实现与其他实现在长度为0的边缘情况下不同。这使得它比其他情况下还要没用。

3
在许多架构上,特别是在memcpy复制不必要的尾随数据时,strlcpymemcpy更快。 strlcpy还返回您错过了多少数据,这可能使您能够更快地进行恢复并减少代码量。 - user14554
1
@jbcreix:我的观点是不应该有任何数据遗漏,在我的memcpy调用中,n是要写入的确切字节数,因此效率也不是太大的问题。 - Alok Singhal
5
如何获取该n值?您只能预先知道缓冲区大小。当然,如果您建议每次使用memcpystrlen重新实现strlcpy,那也可以,但是为什么要停留在 strlcpy 呢?您也不需要一个 memcpy 函数,可以逐个字节复制。参考实现在正常情况下只循环一次数据,对大多数体系结构来说更好。但即使最佳实现使用 strlen + memcpy,仍然没有理由不需要一遍又一遍地重新实现安全的 strcpy。 - user14554
1
Alok,使用strlcpy在好的情况下(这应该是大多数情况),您只需扫描一次输入字符串,在坏的情况下需要扫描两次;而使用strlen+memcpy则总是需要扫描两次。实际上是否有差别是另一回事。 - Patrick Schlüter
4
“*((char *)mempcpy(dst, src, n)) = 0;” - 正确的吗?- 是的,显然是正确的,不失败于代码审查。 - mattnz
显示剩余2条评论

12

我认为Ulrich和其他人认为这会给人一种虚假的安全感。意外截断字符串可能会对代码的其他部分产生安全影响(例如,如果文件系统路径被截断,程序可能无法在预期的文件上执行操作)。


8
例如,一个电子邮件客户端可能会将电子邮件附件的文件名从“malware.exe.jpg”截断为“malware.exe”。 - Chris Peterson
1
@ChrisPeterson 这就是为什么好的开发者总是检查返回值,以便在使用 strl* 函数时知道数据是否被截断并做出相应的处理。 - Tom Lint
"Ulrich和其他人认为这会给人一种虚假的安全感...哈哈,与此同时,Ulrich和他的朋友们经常在BugTraq和Full Disclosure上露面,因为他们的一次性问题。他们应该使用更安全的函数并避免大部分问题。然后他们可以开始告诉别人如何编写更安全的代码..." - jww

7

使用strl函数有两个相关的“问题”:

  1. 必须检查返回值以避免截断。

c1x标准草案作者和Drepper认为程序员不会检查返回值。Drepper说我们应该知道长度并使用memcpy,完全避免使用字符串函数,标准委员会认为安全的strcpy应该在截断时返回非零值,除非由_TRUNCATE标志另有说明。这样做的想法是人们更有可能使用if(strncpy_s(...))。

  1. 不能用于非字符串。

有些人认为字符串函数即使收到错误数据也不应崩溃。这影响标准函数,例如strlen,在正常情况下会导致段错误。新标准将包括许多这样的函数。当然,这些检查会带来性能损失。

与建议中的标准函数相比,使用strl函数的好处是您可以知道错过了多少数据。


请注意,strncpy_s 不是 strncpy 的安全版本,而基本上是 strlcpy 的替代品。 - user14554

6

我认为strlcpystrlcat并不被认为是不安全的,或者至少这不是它们没有被包含在glibc中的原因 - 毕竟,glibc包括strncpy甚至strcpy。

他们受到的批评是它们据称是低效的,而不是不安全的

根据Damien Miller的Secure Portability论文:

strlcpy和strlcat API会正确检查目标缓冲区的边界,无论何时都会以空字符结尾,并返回源字符串的长度,从而可以检测到截断。这个API已经被大多数现代操作系统和许多独立软件包所采用,包括OpenBSD(其起源地)、Sun Solaris、FreeBSD、NetBSD、Linux内核、rsync和GNOME项目。值得注意的例外是GNU标准C库glibc [12],它的维护者坚定地拒绝包含这些改进的API,并将它们称为“可怕低效的BSD垃圾”[4],尽管之前有证据表明它们在大多数情况下比它们替换的API更快[13]。因此,在OpenBSD端口树中存在的100多个软件包维护自己的strlcpy和/或strlcat替代品或等效API,这不是一个理想的状态。
这就是为什么它们在glibc中不可用,但这并不意味着它们在Linux上不可用。它们在libbsd中可用:

它们已经被打包在Debian、Ubuntu和其他发行版中。您也可以直接获取一份副本并在您的项目中使用——它很短且受到宽松许可证的保护:


3
安全性不是二元的。C函数不是完全“安全”或“不安全”的,“安全”或“不安全”的。当使用不正确时,C中的简单赋值操作可能是“不安全”的。当程序员提供必要的正确使用保证时,strlcpy()和strlcat()可以像strcpy()和strcat()一样安全地使用。
所有这些C字符串函数(标准的和非标准的)的主要问题在于它们使安全/安全使用变得容易。安全地使用strcpy()和strcat()并不是易如反掌的;多年来,C程序员的错误使用已经证明了这一点,并引发了许多恶意漏洞和攻击。strlcpy()和strlcat(),以及strncpy()、strncat()、strncpy_s()和strncat_s()都稍微更容易安全使用,但仍然是有难度的。它们是不安全/不安全的吗?如果使用不正确,它们和memcpy()一样危险。

0

strlcpy 可能会触发 SIGSEGV,如果 src 没有以 NUL 结尾。

/* Not enough room in dst, add NUL and traverse rest of src */
if (n == 0) {
    if (siz != 0)
        *d = '\0';      /* NUL-terminate dst */
    while (*s++)
        ;
}

return(s - src - 1);    /* count does not include NUL */

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