日志系统安全:防止日志注入,敏感信息过滤,日志访问控制

日志系统写好了,能跑能查,是不是就完事了?

说实话,我以前也这么想。直到有一次,我在一个嵌入式设备上发现日志文件里被人塞了一堆乱七八糟的控制字符,甚至还有SQL语句片段。那一刻我才意识到——日志系统如果不做安全防护,它本身就是一个巨大的攻击面。

你想想看,日志记录的是系统运行时的“黑匣子”。如果这个黑匣子被人动了手脚,那排查问题的时候,你看到的就不是真相,而是别人想让你看到的假象。更严重的是,敏感信息泄露、日志注入攻击,这些在嵌入式领域一点都不罕见。

所以这一章,咱们就聊聊日志系统的三个安全要点:防止日志注入敏感信息过滤日志访问控制。嗯,这三个点,缺一个都不行。

22.1 防止日志注入:别让日志变成攻击入口

什么叫日志注入?说白了,就是攻击者把恶意数据塞进日志里,让日志解析器、日志查看工具或者下游系统“中招”。

举个例子,你的日志系统如果直接把用户输入的字符串原样写入日志文件,那攻击者就可以在输入里夹带换行符、控制字符,甚至伪造日志条目。我曾经见过一个案例,攻击者在用户名里塞了 \n[ERROR] 系统崩溃,结果运维人员看到日志后误以为系统真的挂了,白白浪费了半天排查时间。

⚠️ 注意: 日志注入不仅仅是“伪造日志”那么简单。如果日志被导入到数据库或SIEM系统,攻击者甚至可以通过日志注入执行SQL注入或命令注入。

怎么防?我的习惯是:对所有写入日志的字符串做转义处理。具体来说,有这几个要点:

  • 转义控制字符:比如换行符 \n、回车符 \r、制表符 \t,这些字符在日志文件中会破坏格式。
  • 过滤不可见字符:ASCII 0-31 的控制字符,除了少数几个(如 \t),其他都应该被替换或删除。
  • 限制日志条目长度:防止攻击者通过超长字符串耗尽日志存储空间。

来看一段我常用的日志安全输出函数:

#include <stdio.h>
#include <string.h>
#include <ctype.h>

/**
 * 安全日志输出:对字符串进行转义,防止日志注入
 * 返回写入的字符数
 */
int safe_log_output(char *buf, size_t buf_size, const char *input) {
    size_t j = 0;
    for (size_t i = 0; input[i] != '\0' && j < buf_size - 1; i++) {
        unsigned char c = (unsigned char)input[i];
        switch (c) {
            case '\n': // 换行符 -> 转义为文字
                if (j + 2 < buf_size) { buf[j++] = '\\'; buf[j++] = 'n'; }
                break;
            case '\r':
                if (j + 2 < buf_size) { buf[j++] = '\\'; buf[j++] = 'r'; }
                break;
            case '\t':
                if (j + 2 < buf_size) { buf[j++] = '\\'; buf[j++] = 't'; }
                break;
            case '\\':
                if (j + 2 < buf_size) { buf[j++] = '\\'; buf[j++] = '\\'; }
                break;
            default:
                // 过滤掉其他控制字符
                if (c >= 0x20 && c <= 0x7E) {
                    buf[j++] = c;
                } else {
                    // 不可见字符替换为 '?'
                    buf[j++] = '?';
                }
                break;
        }
    }
    buf[j] = '\0';
    return j;
}

这段代码的核心思路很简单:白名单策略。只允许可见的ASCII字符通过,其他的一律转义或替换。我在项目中用过这个函数,效果很好,再也没有出现过日志格式被破坏的情况。

💡 小技巧: 如果你用的是C标准库的 fprintf,记得不要直接用 %s 输出用户输入。先经过 safe_log_output 处理,再写入文件。

22.2 敏感信息过滤:别把密码写进日志

这个问题,说实话,我见过太多次了。

有一次,一个同事调试网络通信模块,为了方便,直接把整个数据包用 %s 打印出来了。结果日志里包含了用户的登录密码。幸好那只是开发环境,要是上了生产,这就是妥妥的安全事故。

敏感信息过滤,说白了就是:日志里不该出现的东西,一个字符都不能出现

哪些算敏感信息?我列个清单:

  • 密码、密钥、Token:这些是最高优先级,绝对不能出现。
  • 个人身份信息(PII):比如用户名、邮箱、手机号、身份证号。
  • 内部IP地址、MAC地址:这些信息可能被用来做网络拓扑探测。
  • 文件路径、系统配置:暴露了目录结构,攻击者就有了攻击方向。

怎么过滤?我的做法是:定义敏感字段列表,在日志输出前做正则匹配或字符串替换。对于嵌入式系统,正则可能太重了,我一般用简单的字符串匹配加掩码处理。

#include <stdio.h>
#include <string.h>

/**
 * 敏感信息掩码处理
 * 将字符串中的密码字段替换为 "******"
 * 返回处理后的字符串长度
 */
int mask_sensitive_data(char *buf, size_t buf_size, const char *input) {
    const char *sensitive_keywords[] = {"password", "secret", "token", "key"};
    int keyword_count = 4;
    
    // 先复制原始字符串
    strncpy(buf, input, buf_size - 1);
    buf[buf_size - 1] = '\0';
    
    // 对每个敏感关键词进行掩码
    for (int i = 0; i < keyword_count; i++) {
        char *pos = buf;
        while ((pos = strstr(pos, sensitive_keywords[i])) != NULL) {
            // 找到关键词后面的值(假设格式为 key=value 或 key:value)
            char *value_start = pos + strlen(sensitive_keywords[i]);
            // 跳过等号或冒号
            while (*value_start == '=' || *value_start == ':' || *value_start == ' ') {
                value_start++;
            }
            // 将值替换为掩码
            int mask_len = 6; // "******"
            for (int j = 0; j < mask_len && value_start[j] != '\0' && value_start[j] != ' ' && value_start[j] != ','; j++) {
                value_start[j] = '*';
            }
            pos = value_start + mask_len;
        }
    }
    return strlen(buf);
}

这段代码虽然简单,但在实际项目中够用了。你想想看,攻击者拿到日志后,看到 password=******,他还能干什么?什么都干不了。

⚠️ 注意: 掩码处理不是万能的。如果攻击者能通过日志中的其他信息(比如时间戳、请求ID)关联到原始数据,那掩码就形同虚设。所以,最好的策略是:能不记录就不记录

22.3 日志访问控制:谁可以看,谁可以写

日志文件不是谁都能看的。这一点,我在做嵌入式Linux项目时体会特别深。

有一次,我发现一个普通用户竟然能读取系统日志文件。这意味着什么?意味着他可以看到其他用户的登录记录、操作历史,甚至是一些调试信息里暴露的数据库连接串。这太危险了。

日志访问控制,说白了就是:谁可以写日志,谁可以读日志,谁可以删日志。这三个权限,必须严格区分。

我一般会遵循这几个原则:

  • 最小权限原则:只有日志守护进程有写权限,普通应用只能通过日志API写入,不能直接操作日志文件。
  • 读权限分离:只有运维人员和审计人员可以读日志,普通用户不行。
  • 日志轮转保护:日志轮转时,旧日志文件的权限不能降低。

在Linux系统上,我通常这样设置:

# 创建日志目录,只有root和log组可以访问
sudo mkdir -p /var/log/myapp
sudo chown root:log /var/log/myapp
sudo chmod 750 /var/log/myapp

# 日志文件创建时设置权限
sudo touch /var/log/myapp/app.log
sudo chown root:log /var/log/myapp/app.log
sudo chmod 640 /var/log/myapp/app.log

这样设置之后,只有root用户和log组的成员可以读日志,其他用户连目录都进不去。嗯,这个方案我在多个项目里用过,从来没出过问题。

💡 小技巧: 如果你的系统支持ACL(访问控制列表),可以用 setfacl 做更精细的权限控制。比如,允许某个特定用户只读,不允许写。

22.4 知识体系总览

说了这么多,咱们来画个图,把这三个安全要点串起来。这样你一眼就能看明白,日志系统安全到底要管哪些事。

日志系统安全防护体系 防止日志注入 • 转义控制字符 • 过滤不可见字符 • 限制日志条目长度 • 白名单策略 • 转义函数封装 • 避免直接 %s 输出 敏感信息过滤 • 密码/密钥/Token • 个人身份信息(PII) • 内部IP/MAC地址 • 文件路径/配置 • 掩码处理 • 能不记录就不记录 日志访问控制 • 最小权限原则 • 读写权限分离 • 日志轮转保护 • 目录权限设置 • 文件权限设置 • ACL精细控制 三者缺一不可,共同构建日志系统安全防线

从这张图可以看得很清楚:防止日志注入解决的是“日志内容被篡改”的问题,敏感信息过滤解决的是“日志泄露隐私”的问题,日志访问控制解决的是“谁可以操作日志”的问题。三个维度,缺一不可。

22.5 实战中的避坑指南

最后,分享几个我踩过的坑,希望能帮你少走弯路。

坑一:日志注入的“隐形攻击”

我曾经遇到过一种攻击,攻击者在输入里塞了退格符 \b,导致日志查看器显示的内容和实际内容不一致。比如,日志里写的是 用户登录成功\b\b\b\b失败,显示出来就成了“用户登录失败”。这种攻击很难被肉眼发现,所以一定要对控制字符做转义。

坑二:敏感信息过滤的“漏网之鱼”

有一次,我以为已经把所有的密码字段都掩码了,结果发现日志里还是出现了密码——因为密码被编码成了Base64。从那以后,我养成了一个习惯:对所有看起来像随机字符串的数据都做二次检查。如果拿不准,宁可打上掩码,也不要冒险。

坑三:日志访问控制的“权限漏洞”

我记得有一次,日志轮转脚本把旧日志文件的权限改成了644,结果普通用户也能读了。原因是脚本里用了 cp 而不是 mv,新文件的权限继承的是umask,而不是原文件的权限。所以,日志轮转时一定要显式设置权限,不要依赖默认值。

好了,关于日志系统安全,咱们就聊这么多。记住一句话:日志系统是系统的“眼睛”,但别让这双眼睛被人蒙上


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