概念定义
Host 头是客户端输入。应用一旦把它当配置——当权威域名——它构造的每个 URL 都继承攻击者的控制。最具破坏性的流程是密码重置:受害者点开的链接把重置令牌送进攻击者域名。
工作机制
攻击者带被操纵的 Host 或注入的 X-Forwarded-Host 请求重置流程,邮件渲染的链接便指向该主机。加固方式:基础 URL 一律来自受验证的配置;Host 按环境精确白名单校验;除非代理链逐跳可信,边缘应清理或覆写转发头。
使用场景
审计邮件渲染、重定向生成、OAuth 回调构造、以及一切从请求上下文拼绝对 URL 的代码。按主机名路由的多租户应用要区分“租户查找”(合法)与“URL 构造”(不合法)。
常见误区
“代理会安全地设置 Host”是错觉:除非显式覆写,代理转发客户端所发内容;X-Forwarded-Host 更需显式信任。后缀校验挡不住 evil.example.com。重复 Host 头与绝对 URI 请求目标会在代理与源站间制造解析分歧——那是走私家族;按 Host 变化的缓存若白名单缺失,等于给攻击者按主机名的投毒键。
代码示例
import re
from pathlib import PurePosixPath
from urllib.parse import unquote
NAME_STAR = re.compile(r"filename\*\s*=\s*([^;]+)", re.IGNORECASE)
NAME = re.compile(r'filename\s*=\s*(?:"([^"]*)"|([^;]+))', re.IGNORECASE)
def safe_basename(value: str) -> str | None:
candidate = PurePosixPath(value.replace("\\", "/")).name.strip()
if candidate in {"", ".", ".."} or "\x00" in candidate:
return None
return candidate
def attachment_filename(header: str) -> str | None:
match = NAME_STAR.search(header)
if match:
value = match.group(1).strip().strip('"')
charset, _, remainder = value.partition("'")
_, _, encoded = remainder.partition("'")
try:
decoded = unquote(
encoded or remainder,
encoding=charset or "utf-8",
errors="strict",
)
except (LookupError, UnicodeDecodeError):
decoded = unquote(encoded or remainder)
return safe_basename(decoded)
match = NAME.search(header)
if not match:
return None
value = match.group(1) if match.group(1) is not None else match.group(2)
return safe_basename(unquote(value.strip()))
def main() -> None:
headers = [
'attachment; filename="report final.pdf"',
"attachment; filename=../../etc/passwd",
"attachment; filename*=UTF-8''%E5%B9%B4%E6%8A%A5.pdf",
'attachment; filename=".."',
]
for header in headers:
print(attachment_filename(header))
if __name__ == "__main__":
main()
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync(':memory:');
db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)');
db.prepare('INSERT INTO users (email) VALUES (?)').run('[email protected]');
function findEmails(ids) {
if (ids.length === 0) return [];
if (ids.some((id) => !Number.isInteger(id))) {
throw new TypeError('ids must be integers');
}
const placeholders = ids.map(() => '?').join(', ');
const stmt = db.prepare(
'SELECT id, email FROM users WHERE id IN (' + placeholders + ')',
);
return stmt.all(...ids);
}
try {
// 恶意输入只是参数值,永远不会被解析成 SQL 语法
db.prepare('SELECT * FROM users WHERE email = ?').all("x' OR '1'='1");
} catch (err) {
console.log('injection attempt contained:', err instanceof Error);
}
const rows = findEmails([1, 2, 3]);
console.log('matched rows:', rows.length);
db.close();