22、Web服务与CGI安全:CGI环境变量注入、HTTP头注入、安全的CGI编程

CGI,也就是通用网关接口。说白了,它是Web服务器和外部程序之间的一座桥。你想想看,用户点个按钮、提交个表单,服务器怎么知道该干啥?就是靠CGI把请求转给后台程序,再把结果返回给浏览器。

这座桥要是没修好,攻击者就能顺着它摸进来。我在项目中见过太多因为CGI代码写得糙,导致服务器被日穿的案例。今天咱们就把CGI安全这块掰开揉碎,好好聊聊。

核心观点:CGI程序运行在Web服务器上下文中,拥有文件读写、命令执行等权限。一旦被注入攻击,后果就是服务器沦陷。

CGI安全知识体系 CGI安全 环境变量注入 HTTP头注入 安全CGI编程 QUERY_STRING注入 PATH_INFO篡改 Content-Type伪造 Referer欺骗 输入过滤 最小权限原则

一、CGI环境变量注入——攻击者最爱的入口

Web服务器在调用CGI程序前,会把请求信息塞进环境变量。比如QUERY_STRINGPATH_INFOCONTENT_TYPE这些。CGI程序直接从环境变量里拿数据用,问题就出在这儿。

攻击者可以构造特殊的请求,让环境变量里带上恶意内容。如果CGI程序没做过滤,直接把这些内容拼到命令里执行,那就完蛋了。

典型案例:我曾经审计过一个用C写的CGI程序,它从QUERY_STRING里取参数,然后拼到system()调用里执行系统命令。攻击者传个?cmd=ls;rm -rf /,服务器就炸了。

看个有问题的代码示例:

// 有漏洞的CGI代码
#include <stdlib.h>
#include <string.h>

int main() {
    char *query = getenv("QUERY_STRING");
    char cmd[256];
    
    // 直接拼接,没有过滤!
    snprintf(cmd, sizeof(cmd), "echo %s > /tmp/result.txt", query);
    system(cmd);
    
    return 0;
}

这段代码的问题在哪?攻击者可以传hello;cat /etc/passwdsystem()就会先执行echo hello,再执行cat /etc/passwd。密码文件直接暴露了。

我的建议:永远不要用system()popen()这些函数处理用户输入。如果非要用,必须做严格的输入白名单校验。我个人习惯用execve()配合固定参数,这样用户没法注入额外命令。

二、HTTP头注入——别小看请求头里的猫腻

HTTP头注入,攻击者利用的是CGI程序对请求头处理不当的漏洞。常见的攻击点包括Content-TypeRefererUser-AgentCookie等。

举个例子,有些CGI程序会把User-Agent直接写进日志文件,或者拼到HTML响应里返回给用户。如果User-Agent里带了<script>alert('xss')</script>,那就变成XSS攻击了。

更危险的是,如果CGI程序把HTTP头的内容拼到Location重定向头里,攻击者可以构造%0d%0a(回车换行)来注入额外的HTTP响应头,实现HTTP响应拆分攻击。

// 有问题的HTTP头处理
char *user_agent = getenv("HTTP_USER_AGENT");
char response[1024];

// 直接把用户代理拼进响应
snprintf(response, sizeof(response), 
         "Content-Type: text/html\n\n"
         "<html><body>你的浏览器: %s</body></html>", 
         user_agent);
printf("%s", response);

这段代码里,如果HTTP_USER_AGENTChrome<script>alert(1)</script>,浏览器就会执行这个脚本。

防御要点:对所有从环境变量读取的HTTP头数据,必须做两件事——编码输出长度限制。我一般会限制每个头不超过256字节,超过的直接截断或拒绝。

三、安全的CGI编程——实战中的避坑指南

说了这么多漏洞,咱们聊聊怎么写出安全的CGI程序。我总结了几个关键点,都是血泪教训换来的。

3.1 输入验证是第一道防线

所有从环境变量获取的数据,都要当成不可信的。我习惯写一个统一的输入过滤函数:

// 安全的输入过滤函数
int sanitize_input(const char *input, char *output, size_t out_size) {
    if (input == NULL || output == NULL || out_size == 0) {
        return -1;
    }
    
    size_t j = 0;
    for (size_t i = 0; input[i] != '\0' && j < out_size - 1; i++) {
        // 只允许字母、数字、下划线、连字符
        if (isalnum(input[i]) || input[i] == '_' || input[i] == '-') {
            output[j++] = input[i];
        }
        // 其他字符直接跳过或替换
    }
    output[j] = '\0';
    return 0;
}

避坑指南:我曾经遇到一个项目,开发人员觉得只过滤;|就够了。结果攻击者用$(cat /etc/passwd)这种shell命令替换语法绕过了过滤。所以白名单才是王道,黑名单永远有漏网之鱼。

3.2 最小权限原则

CGI程序不要以root权限运行。我见过太多案例,CGI程序用root跑,一个命令注入漏洞就让攻击者拿到服务器最高权限。

正确的做法是:

  • 创建专用的系统用户运行CGI程序
  • 只给这个用户必要的文件读写权限
  • 使用chroot限制文件系统访问范围
  • 在C代码中调用setuid()/setgid()降权
// 降权示例
#include <unistd.h>
#include <sys/types.h>

int main() {
    // 先降权到nobody用户
    if (setgid(65534) != 0 || setuid(65534) != 0) {
        // 降权失败,直接退出
        return 1;
    }
    
    // 降权成功后才处理请求
    // ... 业务逻辑 ...
    
    return 0;
}

3.3 输出编码不能忘

CGI程序返回给浏览器的内容,如果是HTML格式,必须对特殊字符做编码。尤其是<>&"'这五个字符。

原始字符 HTML实体编码 说明
< &lt; 小于号
> &gt; 大于号
& &amp; 与符号
" &quot; 双引号
' &#x27; 单引号

3.4 缓冲区溢出防护

C语言的CGI程序最容易出缓冲区溢出。攻击者通过超长输入覆盖栈上的返回地址,就能劫持程序执行流程。

我的做法是:

  • 所有字符串操作都用snprintf()strncpy()strncat()这类带长度限制的函数
  • 不要用gets()sprintf()strcpy()strcat()
  • 对输入长度做硬性限制,超过就拒绝
  • 开启编译器的栈保护选项(-fstack-protector-strong

重要提醒:即使你用了snprintf(),也要检查返回值。如果返回值大于等于缓冲区大小,说明数据被截断了,这时候要主动报错,而不是继续用截断后的数据。

3.5 安全的文件操作

CGI程序经常需要读写文件。如果文件名来自用户输入,攻击者可以用../路径穿越来读写任意文件。

// 安全的文件路径处理
int safe_open_file(const char *user_input) {
    char base_path[] = "/var/www/data/";
    char full_path[256];
    
    // 检查是否包含路径穿越字符
    if (strstr(user_input, "..") != NULL ||
        strchr(user_input, '/') != NULL) {
        return -1;  // 拒绝
    }
    
    // 拼接完整路径
    snprintf(full_path, sizeof(full_path), "%s%s", base_path, user_input);
    
    // 打开文件
    FILE *fp = fopen(full_path, "r");
    if (fp == NULL) {
        return -1;
    }
    
    // ... 处理文件 ...
    fclose(fp);
    return 0;
}

我的经验:除了检查../,还要注意空字节注入。攻击者可能在文件名里塞\0来截断字符串。C语言里字符串以\0结尾,如果用户输入valid.txt\0/etc/passwdstrlen()只算到\0前面,但fopen()可能读到后面的内容。所以一定要检查输入里有没有空字节。

四、总结一下

CGI安全说白了就三件事:输入过滤、输出编码、权限控制。你想想看,攻击者能利用的无非就是这三个环节的漏洞。

我做了这么多年安全,见过太多因为CGI代码写得随意导致的安全事故。有些是命令注入,有些是XSS,有些是缓冲区溢出。但归根结底,都是因为开发者没把用户输入当回事。

记住一句话:永远不要信任任何来自客户端的数据。环境变量也好,HTTP头也好,请求体也好,统统当成有毒的来处理。做到这一点,你的CGI程序就安全了一大半。

最后送大家一个检查清单:

  • ✅ 所有环境变量输入都经过白名单过滤
  • ✅ 不使用system()/popen()执行命令
  • ✅ 输出到HTML的内容做了实体编码
  • ✅ 程序以最低权限运行
  • ✅ 所有字符串操作都限制了长度
  • ✅ 文件路径做了穿越防护
  • ✅ 编译时开启了安全选项

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