Well-known URI 为软件发现某个源站的信息提供约定位置。保留的 /.well-known/ 前缀,减少了不同协议各自设计路径时发生名称冲突的机会;注册后缀则指向定义它的具体规范。这个前缀是一种命名机制,不是自动实现,更不是内容可信的保证。服务器仍需正确路由所支持的端点,客户端也必须按照该端点自己的契约解释响应。
按具体后缀的规范实现
RFC 8615 为支持的 URI 方案保留这一命名空间,不要求每个网站发布所有已注册资源,也没有给所有后缀定义统一响应格式。确定媒体类型、认证、跳转处理、缓存或传输条件之前,应查看 IANA 注册及其引用规范。路径看起来熟悉,不能替代协议规则;不同自动化客户端也不会因为共用前缀,就自然采用相同的读取和验证方式。
对于通过网站发布漏洞联系信息,RFC 9116 规定 security.txt 位于 /.well-known/security.txt,并要求通过 HTTPS 获取。旧位置 /security.txt 可以重定向到标准位置。文档使用 UTF-8 纯文本和规定字段,包括至少一个 Contact 以及唯一的 Expires 字段。Contact 提供报告渠道,不代表邮箱一定有人值守,更不能证明某份报告已经被接收或处理。
文档中的 Expires 字段用时间戳限制信息的新鲜度,它与 HTTP 响应中的 Expires 头不是同一个东西。传输层返回二百,不会让已经过期的联系信息重新变得有效。地址和政策需要持续维护,并在内容过时之前更新。使用 Canonical、Policy 等可选字段时,也应遵守各自规则,不能把文档里任意一个网址都当成已经验证的可信声明。
坏掉的样子
宽泛的应用回退路由可能对任何未知地址都返回首页或登录页,并统一使用二百状态。浅层健康检查认为路径可达,真正的协议客户端却得不到可用文档。因此要保留准确路由,同时检查媒体类型与内容。意外跳转到另一个源站,还可能改变信任上下文,而不只是移动一个文件。仅检查最终状态成功,可能遗漏这些协议层面的错误。
命名空间本身不会认证内容。HTTPS 校验帮助建立到目标主机的连接,文档验证及协议要求的其他检查,才进一步判断内容是否可用。存在安全联系信息,不等于授权外部人员扫描或攻击服务。部署也不能因为一个标准资源计划公开,就不加区分地暴露整个目录。应围绕实际支持的资源配置路由和权限,而不是把前缀当成自动开放所有内容的规则。
对没有实现的后缀返回四零四,与悄悄生成一份通用成功文档,是不同选择。每个受支持端点都应有维护者和适合其协议的检查。注册表描述的是约定,既不能证明某个源站已经实现,也不能证明当前响应已经验收。协议发现减少的是查找位置的不确定性,并没有取消维护、格式检查与可信性判断这些后续工作。
快速验证
保存为 well-known.py,使用 Python 3.14 执行 python3 well-known.py。实验仅提供回环 HTTP 服务,联系地址使用明确虚构的 example.invalid。它验证路由及选定文档字段;由于没有使用 HTTPS,不能把它视为完整 RFC 9116 部署或传输安全测试。代码不会发送邮件,也不会连接真实的漏洞接收系统。
from datetime import datetime, timedelta, timezone
from http.client import HTTPConnection
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from threading import Thread
import json
now = datetime.now(timezone.utc)
future = (now + timedelta(days=30)).isoformat().replace('+00:00', 'Z')
past = (now - timedelta(days=1)).isoformat().replace('+00:00', 'Z')
content = {'text': f'Contact: mailto:security@example.invalid\nExpires: {future}\n'}
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
status = 200 if self.path == '/.well-known/security.txt' else 301 if self.path == '/security.txt' else 404
data = content['text'].encode() if status == 200 else b''
self.send_response(status)
if status == 301:
self.send_header('Location', '/.well-known/security.txt')
self.send_header('Content-Type', 'text/plain; charset=utf-8')
self.send_header('Cache-Control', 'no-store')
self.send_header('Content-Length', str(len(data)))
self.end_headers()
self.wfile.write(data)
def log_message(self, *args):
pass
def check_contact_and_expiry(text):
fields = dict(line.split(': ', 1) for line in text.splitlines())
if not fields.get('Contact', '').startswith('mailto:'):
raise ValueError('missing demonstration contact')
expires = datetime.fromisoformat(fields['Expires'])
if expires <= now:
raise ValueError('expired metadata')
server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
thread = Thread(target=server.serve_forever)
thread.start()
try:
def fetch(path):
connection = HTTPConnection('127.0.0.1', server.server_port, timeout=5)
try:
connection.request('GET', path)
response = connection.getresponse()
return {'status': response.status, 'body': response.read().decode(),
'location': response.getheader('Location'),
'content_type': response.getheader('Content-Type')}
finally:
connection.close()
valid = fetch('/.well-known/security.txt')
assert valid['status'] == 200 and valid['content_type'] == 'text/plain; charset=utf-8'
check_contact_and_expiry(valid['body'])
legacy = fetch('/security.txt')
assert legacy['status'] == 301 and legacy['location'] == '/.well-known/security.txt'
assert fetch('/.well-known/unimplemented')['status'] == 404
rejected = []
for text in (content['text'].replace(future, past), f'Expires: {future}\n'):
content['text'] = text
response = fetch('/.well-known/security.txt')
assert response['status'] == 200
try:
check_contact_and_expiry(response['body'])
except ValueError as error:
rejected.append(str(error))
assert rejected == ['expired metadata', 'missing demonstration contact']
print(json.dumps({'valid_status': valid['status'], 'legacy_status': legacy['status'],
'unimplemented_status': 404, 'rejected_200_documents': rejected,
'https_transport_tested': False}))
finally:
server.shutdown()
thread.join()
server.server_close()
标准路径第一次返回二百、预期纯文本类型、联系字段,以及三十天后的到期时间。旧路径返回三零一,指向标准位置;未实现的后缀返回四零四。随后脚本在同一成功路由上提供两份无效文档:一份已经过期,另一份缺少联系字段。它们的 HTTP 状态仍然是二百,却都被字段检查拒绝。这说明地址可达与元数据可用必须分别验证。
这里的解析器刻意只处理已知演示格式,没有实现通用客户端需要的全部重复字段、签名、Canonical 检查、跳转与 URI 校验。通过两个字段检查,不等于整份文档完全符合标准,也没有验证联系渠道真实可用。应该用这个实验区分位置、HTTP 成功与可用元数据,再根据选定协议的完整要求验证实际部署,最后才发布真实端点。
来源
- RFC 8615: Well-Known URIs,保留命名空间与注册模型,已核验 2026-09-13。
- RFC 9116: security.txt,规定路径、HTTPS 传输、联系字段与到期规则,已核验 2026-09-13。
- IANA: Well-Known URIs registry,已注册后缀及其定义规范,已核验 2026-09-13。