失效的假设 “装上就能用,说明默认配置是合理的、安全的”
所属组 W7 · 配置与暴露
和前面的不同 前面是"代码写错了";这一篇是"代码没错,配置默认开着不该开的"
复现环境 Python 3 标准库,无外部依赖

一、你没写错代码,只是信了默认值

你什么代码都没写错。你只是用默认配置起了一个服务。

然后有人访问 /.git/,把你整个仓库下载走了。

前十九篇的漏洞,多少都要"写错点什么"。这一篇不用——你可以一行代码都没写错,仅仅因为用了默认配置,就把系统敞开了。

打个比方

你买了个路由器,插上就能用。它的管理后台密码是 admin

这不是厂商偷懒。如果出厂时每台机器都是随机密码,客服电话会被打爆——总有人找不到那张贴纸。所以"admin/admin"是厂商在"能不能用起来"和"安不安全"之间做的选择,而他们选了前者。这个选择对厂商是对的。

问题是:这台机器现在在你家,而那个选择还挂在上面。

软件的默认配置全是同一个逻辑。默认要让新手最快跑起来、能看到详细报错方便调试、能免密码进管理台——每一条都是为"别卡住新用户"优化的,不是为"上生产"优化的。

⭐ 所以失效的假设是:把"默认值"当成了"经过安全考量的推荐值"。它不是。它是"最不让新用户卡住的值"。

import threading, tempfile, os, urllib.request, functools
from http.server import HTTPServer, SimpleHTTPRequestHandler

root = tempfile.mkdtemp()                       # 一个"用默认配置起的静态服务"
os.makedirs(os.path.join(root, ".git"))
open(os.path.join(root, ".git", "config"), "w").write("[remote]\n  url = git@internal:core.git\n")
open(os.path.join(root, "db.sql.bak"), "w").write("-- 数据库备份\nINSERT INTO users ...")
open(os.path.join(root, "index.html"), "w").write("<h1>hi</h1>")

class Quiet(SimpleHTTPRequestHandler):
    def log_message(self, *a): pass

srv = HTTPServer(("127.0.0.1", 0), functools.partial(Quiet, directory=root))
threading.Thread(target=srv.serve_forever, daemon=True).start()
base = f"http://127.0.0.1:{srv.server_address[1]}"
get = lambda p: urllib.request.urlopen(base + p).read().decode()

print("根目录有 index.html,一切正常:", "<h1>hi</h1>" in get("/"))
print("但 .git/ 下没有 index —— 默认行为是【列出目录】:", "config" in get("/.git/"))
print("于是能直接下载 .git/config:")
for line in get("/.git/config").strip().splitlines():
    print("   ", line)
print("备份文件同样能拿:", get("/db.sql.bak").splitlines()[0])
srv.shutdown()
根目录有 index.html,一切正常: True
但 .git/ 下没有 index —— 默认行为是【列出目录】: True
于是能直接下载 .git/config:
    [remote]
      url = git@internal:core.git
备份文件同样能拿: -- 数据库备份

⭐ 每一项单独看都不是 bug,是"功能":调试模式帮你看报错、目录列举帮你浏览文件、详细错误帮你定位问题、默认口令帮你首次登录。它们只是在生产环境里不该开着。 失效的假设是把"默认值"当成了"经过安全考量的推荐值"——它不是,它是"最不让新用户卡住的值"。

二、逐项:它们各自把什么交了出去

调试模式        报错页直接打印堆栈、源码片段、有时连环境变量(含密钥)一起吐出来
               —— 攻击者故意触发一个错误,就读到了你的内部
默认/弱口令     admin/admin 之类;扫描器几秒就试出来,直接进管理后台
目录列举        能列出目录 => 看到 .git/、备份文件 config.bak、日志、上传目录
               很多"拖库"就是先目录列举,发现一个 database.sql.bak
管理端口公网开放  数据库、管理面板、监控台、调试接口本该只在内网,却绑了 0.0.0.0
详细错误回显    "用户名不存在"vs"密码错误"这种差别,就够枚举账号(呼应下一组认证)

⚠️ 这一节最该改掉的一个习惯,是把这些当成"运维的事"。它们看起来不像漏洞——没有 payload,没有巧妙的构造,读起来像一份检查清单,很容易一眼扫过去。可它们的利用成本是这份清单里最低的:不需要理解任何机制,一个扫描器就够了。

⚠️ 共同点是:它们不是被"攻破"的,是被"发现"的。 攻击者不需要漏洞利用,只需要扫描——发现一个开着的调试接口、一个默认口令、一个可列举的目录。这类"暴露面"是自动化扫描器的主食,一个新部署上线几分钟内就会被互联网背景扫描摸一遍。

三、根治:默认拒绝,显式开放

修复的思路不是"逐个记得关掉危险项"——那又回到了黑名单(你会漏)。而是反过来:默认什么都不暴露,需要什么再显式打开。

错的心智:  用默认配置起步,然后"记得"关掉调试、改掉口令、关掉列举……
           —— 靠记性,漏一个就是一个洞
对的心智:  生产配置默认最小暴露(debug off、无默认口令、端口只绑内网),
           某个功能确实需要,才显式、有理由地开

⭐ 这就是"默认拒绝"(第 12 篇纵向越权、第 14 篇 CORS、第 17 篇解析歧义)在配置层的又一次现身。整份清单里这条原则出现了太多次,因为它是对抗"人会遗漏"的唯一可靠办法:让安全成为不需要记住的默认,让暴露成为需要理由的例外。

落地手段:

① 环境分离           生产配置 ≠ 开发配置;debug/详细错误只在本地
② 配置基线 + 扫描     有一份"生产该长什么样"的基线,CI/上线前自动核对
③ 最小暴露           端口默认绑内网、管理面板放跳板机后、删掉示例/默认账号
④ 别让报错说太多      对外统一模糊错误,详细信息只进服务端日志

这一类没有单一的标志性公开事件,它以大量分散的个案存在;有技术细节的会收进事件分析

⭐ 这类事故的共同措辞是"数据没有被黑客攻破,而是本来就是公开的"——没有漏洞利用,没有攻击链,只是配置把门开着。它提醒我们:不是所有泄露都需要"攻击",很多只需要"发现"。

四、防御

① 默认拒绝、显式开放         生产默认最小暴露,功能按需打开并留下理由
② 环境分离                 debug/verbose/示例账号绝不进生产
③ 配置基线自动核对          上线前用清单/扫描核对"生产该有的样子"
④ 改掉一切默认凭据          部署第一步就是换掉默认口令、删示例账号
⑤ 最小网络暴露             管理/数据/调试端口默认内网;对外只开必要的
⑥ 对外错误信息统一模糊       细节进日志,不进响应

判断标准:把你的生产服务当成陌生人,用扫描器扫一遍自己——它能发现什么? 你没扫的,攻击者会替你扫。

五、这些防御的边界

① 配置漂移会让基线失效。 上线时合规,不代表三个月后还合规——有人临时开了个调试口没关、加了个服务用了默认配置。基线要持续核对,不是一次性检查。

② “默认安全"依赖你用的软件本身。 有些软件的默认就是危险的(默认公开、默认无认证)。你不能假设"没动配置 = 安全”,必须主动核对每个组件的默认到底是什么——尤其是数据库、缓存、消息队列这类基础设施。

③ 暴露面比你以为的大。 除了应用本身,还有:云存储桶权限、DNS 记录指向的废弃服务、测试/预发环境、第三方集成的回调地址。资产清单不全,就总有一个没上锁的门(呼应事件分析里 Equifax 那类"没扫到那台机器")。

关于时效:具体软件的默认配置随版本变化——有的新版本把危险默认改安全了(如某些数据库新版默认只绑本地),有的引入了新的默认开关。任何"某软件默认安全/危险"的判断都要对着你用的那个版本核实,本篇不钉具体产品。

六、本篇小结

你可以一行代码没写错,仅因为用了默认配置就把系统敞开。
默认值是为"开箱能跑"优化的,不是为"安全"优化的。

⭐ 每一项都是"功能"不是 bug,只是不该在生产开着:
   调试模式(吐堆栈/密钥) · 默认弱口令 · 目录列举(.git/备份) · 管理端口公网 · 详细报错
⚠️ 它们不是被"攻破"的,是被"扫描发现"的 —— 很多泄露不需要攻击,只需要发现

⭐ 根治:默认拒绝、显式开放(配置层的"默认拒绝",第四次出现在本清单)
   让安全成为不用记住的默认,让暴露成为需要理由的例外

判断:把自己的生产当陌生人扫一遍,能发现什么?你没扫的,攻击者会扫。

思考题

  1. §一 说默认配置是为"开箱能跑"而非"安全"优化的。举两个你用过的软件,其默认配置里"方便"和"安全"直接冲突的地方。
  2. 为什么"逐个记得关掉危险项"是黑名单思路、注定会漏?把它和第 1 篇"黑名单必输"、第 12 篇"默认拒绝"连起来说明。
  3. §二 说这些暴露"不是被攻破的,是被发现的"。这对"我们没有已知漏洞,所以是安全的"这种想法意味着什么?“没有漏洞"和"没有暴露"是一回事吗?
  4. 目录列举让攻击者看到 .git/ 会有什么后果?(提示:和下一篇"密钥进了版本库"联系起来——.git/ 暴露等于整个仓库历史暴露。)
  5. §六① 说"配置漂移会让基线失效”。设计一个机制:让"生产配置偏离基线"这件事能被自动、持续地发现,而不是靠人定期检查。
  6. §六③ 说"暴露面比你以为的大",并提到废弃服务、预发环境。为什么这些"边角"往往是最先失守的?它和 Equifax"没扫到那台机器"是同一类问题吗?
  7. “把自己当陌生人扫一遍"这个判断标准,和第 11 篇越权测试、第 17 篇差异测试有什么共同的思路?为什么"从攻击者视角测自己"这件事,防御方总是做得不够?

相关第 12 篇:纵向越权 · 第 21 篇:密钥进了版本库 · Web 安全机制篇 · 事件分析