5.8. 人类语言(区域设置)选择

随着越来越多的人拥有计算机和互联网,程序支持多种人类语言和文化的需求也日益增加。语言和其他文化因素的这种组合通常被称为“区域设置”。修改程序以使其能够支持多种区域设置的过程称为“国际化”(i18n),而为程序提供特定区域设置信息的过程称为“本地化”(l10n)。

总的来说,国际化是一件好事,但这个过程也为安全漏洞提供了另一个机会。由于潜在不受信任的用户提供有关所需区域设置的信息,因此区域设置选择成为另一种输入,如果未得到适当保护,则可能被利用。

5.8.1. 如何选择区域设置

在本地运行的程序(包括 setuid/setgid 程序)中,区域设置信息由环境变量提供。因此,与所有其他环境变量一样,必须在使用前提取这些值并对照有效模式进行检查。

对于 Web 应用程序,此信息可以从 Web 浏览器(通过 Accept-Language 请求标头)获得。但是,由于并非所有 Web 浏览器都能正确传递此信息(也并非所有用户都正确配置了他们的浏览器),因此这种情况并不像您想象的那么常用。通常,Web 浏览器中请求的语言只是作为表单值传递。同样,与任何其他表单值一样,必须在使用前检查这些值的有效性。

在任何情况下,区域设置信息实际上只是前面章节中讨论的输入的一个特例。但是,由于很少考虑此输入,因此我将其单独讨论。特别是,当与格式字符串(稍后讨论)结合使用时,用户控制的字符串可能允许攻击者强制其他程序运行任意指令、破坏数据以及执行其他不幸的操作。

5.8.2. 区域设置支持机制

在类 Unix 系统上,有两种主要的库接口用于支持区域设置选择的消息,一种称为“catgets”,另一种称为“gettext”。在 catgets 方法中,每个字符串都被分配一个唯一的数字,该数字用作消息表的索引。相比之下,在 gettext 方法中,一个字符串(通常是英语)用于查找翻译原始字符串的表。catgets(3) 是一个被接受的标准(通过 X/Open 可移植性指南第 3 卷和 Single Unix Specification),因此您的程序可能正在使用它。“gettext”接口不是官方标准(尽管它最初是 UniForum 的提议),但我认为它是使用更广泛的接口(Sun 和几乎所有 GNU 程序都使用它)。

理论上,catgets 应该稍微快一些,但这在今天的机器上充其量只是微不足道的,并且保持 catgets() 中唯一标识符有效的簿记工作使得 gettext() 接口更容易使用。我建议使用 gettext(),仅仅因为它更容易使用。但是,不要只听我的;有关更长更详细的比较,请参阅 GNU 关于 gettext 的文档 (info:gettext#catgets)。

特别是 catgets(3) 调用(及其相关的 catopen(3) 调用)容易受到安全问题的攻击,因为环境变量 NLSPATH 可用于控制用于获取国际化消息的文件名。GNU C 库忽略 setuid/setgid 程序的 NLSPATH,这有所帮助,但这并不能保护在其他实现上运行的程序,也不能保护其他程序(如 CGI 脚本),这些程序“似乎”不需要这种保护。

据我所知,广泛使用的“gettext”接口至少不易受到恶意 NLSPATH 设置的影响。但是,在我看来,恶意设置 LC_ALL 或 LC_MESSAGES 可能会导致问题。此外,如果您在其文件 cat-compat.c 中使用 gettext 的 bindtextdomain() 例程,则它确实依赖于 NLSPATH。

5.8.3. 合法值

目前,如果您必须允许不受信任的用户设置有关他们所需区域设置的信息,请确保提供的国际化信息符合仅允许合法区域设置名称的狭窄过滤器。对于用户程序(尤其是 setuid/setgid 程序),这些值将通过 NLSPATH、LANGUAGE、LANG、旧的 LINGUAS、LC_ALL 和其他 LC_* 值(尤其是 LC_MESSAGES,但也包括 LC_COLLATE、LC_CTYPE、LC_MONETARY、LC_NUMERIC 和 LC_TIME)传入。对于 Web 应用程序,此用户请求的语言信息集将通过 Accept-Language 请求标头或表单值完成(应用程序应通过 Content-Language 标头指示返回数据的实际语言设置)。如果您的用户可以设置您的环境变量(即 setuid/setgid 程序),或者作为输入过滤的一部分(例如,对于 CGI 脚本),您可以检查此值作为环境变量过滤的一部分。GNU C 库 “glibc” 不接受 setuid/setgid 程序的某些 LANG 值(特别是任何带有 “/” 的值),但在该过滤中发现了错误(例如,Red Hat 发布了一个更新,以修复 2000 年 9 月 1 日 glibc 中的此错误)。任何标准都不要求这种过滤,因此您自己进行过滤更安全。我没有找到任何关于过滤语言设置的指南,因此以下是基于我对该问题研究的建议。

首先,关于这些设置的合法值的一些说明。语言设置通常使用 IETF RFC 1766 中定义的标准标记进行设置(该标准标记使用两个字母的国家/地区代码作为其基本标记,后跟一个可选的子标记,用破折号分隔;我发现环境变量设置使用下划线代替)。但是,有些人认为这不够灵活,因此可能很快也会使用三个字母的国家/地区代码。此外,还有两种主要的、不太兼容的扩展格式:X/Open 格式和 CEN 格式(欧洲共同体标准);您应该允许这两种格式。典型值包括 “C”(C 区域设置)、“EN”(英语)和 “FR_fr”(法语,使用法国的惯例)。此外,由于太多人使用非标准名称,程序不得不开发 “别名” 系统来应对非标准名称(对于 GNU gettext,请参阅 /usr/share/locale/locale.alias,对于 X11,请参阅 /usr/lib/X11/locale/locale.alias;您可能需要 “aliases” 而不是 “alias”);通常也应该允许它们。像 gettext() 这样的库必须接受所有这些变体,并在可能的情况下找到合适的值。进一步信息的来源之一是 FSF [1999];另一个来源是 li18nux.org 网站。过滤器不应允许不需要的字符,特别是 “/”(这可能允许逃脱受信任的目录)和 “..”(这可能允许向上移动一个目录)。NLSPATH 中其他危险字符包括 “%”(表示替换)和 “:”(目录分隔符);我拥有的其他机器的文档表明,某些实现可能会将它们用于其他值,因此最安全的做法是禁止它们。

5.8.4. 结论

简而言之,我建议简单地擦除或重置 NLSPATH,除非您有受信任的用户提供该值。对于 HTTP 中的 Accept-Language 标头(如果您使用它)、指定区域设置的表单值以及环境变量 LANGUAGE、LANG、旧的 LINGUAS、LC_ALL 和上面列出的其他 LC_* 值,请从不受信任的用户处过滤区域设置,以仅允许空(空)值或仅允许完全匹配此正则表达式的值(请注意,我最近添加了 “=”)
 [A-Za-z][A-Za-z0-9_,+@\-\.=]*
我还没有找到任何不符合此模式的合法区域设置,但此模式似乎可以防止区域设置攻击。当然,不能保证请求的区域设置中提供消息,但在这种情况下,这些例程将回退到默认消息(通常是英语),这至少不是安全问题。

如果您希望非常挑剔,并且只匹配 li18nux 的区域设置模式的模式,则可以使用此模式代替
 ^[A-Za-z]+(_[A-Za-z]+)?
 (\.[A-Z]+(\-[A-Z0-9]+)*)?
 (\@[A-Za-z0-9]+(\=[A-Za-z0-9\-]+)
  (,[A-Za-z0-9]+(\=[A-Za-z0-9\-]+))*)?$
在这两种情况下,这些模式都使用 POSIX 的扩展(“现代”)正则表达式表示法(请参阅类 Unix 系统上的 regex(3) 和 regex(7))。

当然,如果没有表示其书写符号的标准方法,就无法支持语言,这使我们想到了字符编码的问题。