Skip to content

Database Backup & Log Leak Defense — 数据库备份、日志与运维残留防护 ​

一个暴露在根目录的 backup_20250101.sql 或未遮罩的 runtime/log/ 足以让全部注入防御形同虚设。蓝队必须从存储位置、备份加密、日志遮罩、安装清理与管理工具隔离五条线全面封堵。

1. 运维泄露风险与加固映射 ​

对应 05-backup-log-leak.md 中的五类高发运维泄露面,建立对应控制措施:

泄露类别典型暴露路径危害本质蓝队加固要求
SQL 与压缩备份/backup.sql, /db.zip, /backup_YYYYMMDD.sql.gz攻击者按域名+日期字典直接下载整库数据与表结构严禁落盘至 Webroot;加密后推送至私有对象存储,文件名使用随机 UUID
数据库与应用日志general.log, slow.log, /storage/logs/, /runtime/log/暴露含明文参数 SQL、DSN、Token 及支付回调 raw_body/sign生产关闭 general_log;日志目录外移并启用敏感字段掩码化过滤器
安装程序残留/install/, /install/install.sql, install.lock泄露建表脚本与默认管理员哈希,甚至被二次重装覆盖配置构建/部署阶段强制删除 /install,而非仅靠 install.lock
Web 数据库管理面板/phpmyadmin/, /pma/, /adminer.php直接暴露高危管理入口,易遭弱口令爆破或版本漏洞利用生产环境彻底卸载;如确需应急使用,仅限 SSH 隧道/VPN + MFA 访问
Git 历史与迁移脚本/.git/HEAD, migrations/, seeders/恢复历史硬编码凭证、测试种子数据与路由逻辑构建打包排除 .git,禁止将真实账号或密钥写入迁移/种子文件

2. 关键加固实施细则 ​

2.1 加密备份管线与对象命名随机化 ​

禁止使用 backup_YYYYMMDD.sql 命名并保存在站点目录下。推荐通过管道直接压缩并公钥加密后上传至禁用公开读权限的私有存储桶:

bash
#!/usr/bin/env bash
set -euo pipefail
BACKUP_UUID="$(cat /proc/sys/kernel/random/uuid)"
TARGET_OBJ="s3://private-db-backups/prod/${BACKUP_UUID}.sql.zst.age"

# 1. 不落地明文 SQL,流式压缩并用接收端公钥加密
mysqldump --single-transaction --routines --triggers shop_db \
  | zstd -3 \
  | age -r "age1examplepublickeyplaceholder0000000000000000000000000000" \
  | aws s3 cp - "${TARGET_OBJ}" --sse aws:kms

同时在 Nginx 层增加兜底阻断规则,拒绝任何 .sql、.dump、.tar.gz、.zip、.rar、.7z、.log 文件下载请求。

2.2 数据库日志与支付回调日志掩码化 ​

  1. MySQL 日志配置:生产库默认保持 general_log = OFF;若因排障临时开启慢查询日志 slow_query_log = ON,日志路径必须位于 /var/log/mysql/(权限 0640 mysql:adm),并在排查结束后立即关闭或限制采样率,防止 INSERT INTO cards/users 完整语句长期滞留。
  2. 应用层日志过滤器:对 ThinkPHP /runtime/log/、Laravel /storage/logs/ 及支付回调日志,必须在写入前对 DSN 密码、Authorization/token/sign/card_key/password 及银行卡号/手机号进行掩码化(遮罩)替换,且日志目录绝对不能位于 Web 根目录内。

2.3 消除安装残留与 Web 管理面板 ​

  • Dockerfile / CI 清理指令:
dockerfile
# 构建阶段强制移除安装目录与任何潜在管理脚本
RUN rm -rf /var/www/html/install \
           /var/www/html/phpmyadmin \
           /var/www/html/adminer*.php \
           /var/www/html/*.sql
  • SSH 隧道替代公网面板:运维连接数据库优先使用跳板机 + 短有效期证书或 ssh -L 13306:127.0.0.1:3306 ops-bastion,不在公网部署 phpMyAdmin / Adminer。

3. 可运行防御自检脚本 ​

以下 Python 脚本包含两个可复用组件:(1)应用日志敏感字段遮罩过滤器(掩码化 DSN 口令、Token、支付签名、卡密与手机号);(2)部署目录残余备份/安装脚本/管理面板检出器,附完整自检断言:

python
#!/usr/bin/env python3
"""Self-check for log secret masking and backup/install/adminer remnant detection."""
import re
import tempfile
from pathlib import Path

LOG_MASK_RULES = [
    # 遮罩数据库连接串中的用户名与密码
    (re.compile(r"((?:mysql|pgsql|postgres|redis|mongodb)://)([^:/\s]+):([^@\s]+)@", re.I), r"\1***:***@"),
    # 遮罩常见密钥、令牌、签名与卡密键值对
    (re.compile(r"(?i)\b(api[_-]?key|token|secret|password|sign|card_key|pay_key)\b(\s*[:=]\s*['\"]?)([^'\"\s&,]+)"), r"\1\2[REDACTED]"),
    # 遮罩大陆手机号中间四位
    (re.compile(r"\b(1[3-9]\d)\d{4}(\d{4})\b"), r"\1****\2"),
]

DANGEROUS_ARTIFACTS = re.compile(
    r"(?:^|/)(?:install(?:/|$)|phpmyadmin|pma|adminer|sqlbuddy)|"
    r"\.(?:sql|dump|sql\.gz|tar\.gz|zip|rar|7z|log)$",
    re.I,
)


def mask_log_line(line: str) -> str:
    masked = line
    for rx, repl in LOG_MASK_RULES:
        masked = rx.sub(repl, masked)
    return masked


def find_exposed_artifacts(webroot: Path) -> list[str]:
    hits = []
    for p in webroot.rglob("*"):
        rel = p.relative_to(webroot).as_posix()
        if DANGEROUS_ARTIFACTS.search(rel):
            hits.append(rel)
    return sorted(hits)


def run_self_check() -> None:
    # 1. 日志敏感字段遮罩测试
    raw_log = (
        "2026-11-12 ERROR connect mysql://dbuser:S3cretPass!@10.0.0.5:3306/shop "
        "notify out_trade_no=20261112001 sign=9f8e7d6c5b4a phone=13812345678 card_key=CDK-9988-7766"
    )
    safe_log = mask_log_line(raw_log)
    assert "S3cretPass!" not in safe_log
    assert "9f8e7d6c5b4a" not in safe_log
    assert "1234" not in safe_log
    assert "CDK-9988-7766" not in safe_log
    assert "20261112001" in safe_log  # 保留可追踪订单号用于排障

    # 2. 部署目录备份/安装/面板残留检测
    with tempfile.TemporaryDirectory() as tmp:
        root = Path(tmp)
        (root / "install").mkdir()
        (root / "install" / "install.sql").write_text("CREATE TABLE users;", encoding="utf-8")
        (root / "backup_20261112.sql").write_text("INSERT INTO orders;", encoding="utf-8")
        (root / "adminer.php").write_text("<?php // adminer", encoding="utf-8")
        (root / "assets").mkdir()
        (root / "assets" / "app.js").write_text("console.log(1);", encoding="utf-8")

        exposed = find_exposed_artifacts(root)
        assert "install" in exposed
        assert "install/install.sql" in exposed
        assert "backup_20261112.sql" in exposed
        assert "adminer.php" in exposed
        assert "assets/app.js" not in exposed
    print("[PASS] Backup & log leak defense self-check succeeded.")


if __name__ == "__main__":
    run_self_check()

防御工作流 ​

text
1. 盘点现有站点与容器目录,清理所有 .sql/.dump/.zip/.tar.gz 备份、/install 目录与 phpMyAdmin/Adminer 文件
2. 将备份作业迁移至流式压缩+加密后直传私有对象存储,对象键采用 UUID 随机命名并配置最小 IAM 权限
3. 核查 MySQL general_log 状态为 OFF,将应用日志目录移出 Webroot 并设置 0640 文件权限
4. 在日志组件及支付回调记录入口接入敏感字段遮罩过滤器(DSN/Token/签名/卡密/手机号)
5. 在 Nginx 拦截归档/日志后缀并禁止 /.git 与管理面板路径,接入 CI 发布门禁扫描残留物

验证与判定标准 ​

防护域验证手段合格标准失败信号
备份文件暴露按站点名+日期字典及后缀探测 Web 根目录所有 .sql/.zip/.tar.gz/.dump 路径均返回 404命中 HTTP 200 大体积文件或返回 403 暴露目录存在
数据库与应用日志检查 MySQL 变量与最新业务日志内容general_log=OFF;日志外置且无明文密码/签名/卡密/手机号公网可读 runtime/log 或日志含 INSERT 明文敏感记录
安装文件清理检查容器镜像与线上 /install/ 路径构建产物中无 /install/ 与 install.sql,请求返回 404存在 install.sql 或可通过 POST 重装覆盖配置
管理面板隔离扫描 /phpmyadmin/、/adminer.php 等常见路径线上无对应文件且端口仅内网/SSH 隧道可达公网可打开数据库登录页

MCP 工具映射 ​

防御步骤MCP 工具说明
检索备份与日志防御kb_router按 backup security、log masking、install remnant 搜索
对照高危字典路径kb_read_file读取 05-backup-log-leak.md 获取常见备份与面板路径列表
目录浏览kb_catalog浏览 24-database 全部防御文档
残留路径回归验证http_probe在 red/both 模式下批量验证候选备份、日志、安装与面板路径不可达

关联技术 ​

Apache-2.0 · 仅供授权环境下的学习与防御性研究使用 · 免责声明