fmt
fmt 安全边界:格式控制权、参数生命周期与输出编码
分清不可信文本和运行期格式程序,保留借用数据的生命周期,不把类型安全或有界写入当成完整的输出安全。
你将学会
- 把数据与格式控制权分开
- 复核跨越异步边界的数据
- 把输出目标当作另一种协议
开始前需要
- 理解基础 C++ 值、引用、字符串与构建目标
- 能够区分文档预期与实际测量结果
追踪具体 fmt 行为,在输出、生命周期和验证边界明确的前提下选择集成方式。
先看结论
- 用户文本与用户编写的运行期模板授予不同控制权。
- 被借用的数据必须活得比延迟格式化操作更久。
- 类型安全不替代资源限制、输出编码或敏感信息处理。
把数据与格式控制权分开
当用户输入只是一个值时,可将其放在 fmt::format("{}", text) 这样的固定字段后面。如果允许调用方提供经过 fmt::runtime 包装的字符串,就同时给予了它字段、呈现规则和大小方面的控制权。模板编辑器可能确实需要这种能力,但必须明确限制与失败处理。
资源问题不只有非法格式。语法合法但特别大的宽度、长字符串或自定义 formatter 的工作,都可能消耗时间和内存。应在格式化前限制应用接受的值和模板功能。format_to_n 只限制目标写入,其未截断计数行为并不能建立完整的执行预算。
复核跨越异步边界的数据
公开参数视图并不拥有引用对象。队列若保存 fmt::format_args 等待稍后使用,可能超过底层参数存储或字符串的生命周期。动态参数存储会复制部分类型,但字符串视图的指向数据和显式引用包装器仍然是借用关系。容器名字听起来方便,不等于已经完成所有权审查。
有源码依据的反例分别把字符数组按复制值、显式引用和字符串视图插入,再修改首字符。副本输出仍是 abc,借用形式则变成 Xbc。正确结论不是禁止借用,而是必须把数据所有者与存活时间写进队列契约,不能仅依据参数已经“放进容器”来判断安全。
把输出目标当作另一种协议
fmt 生成格式化文本,但不是 HTML 清理器、SQL 参数绑定器或敏感信息脱敏策略。调试呈现可以改善可读性,却不能建立另一个输出场景的转义规则。确定哪些数据允许离开应用之后,还需要使用目标场景正确的编码方法或结构化 API。
对固定缓冲区,应保存明确的实际写入长度,并决定怎样报告截断。UTF-8 字节截断与缺少终止符分别需要处理。仓库的测试与模糊测试说明是有价值的质量依据,但本次编辑复核不是全面安全审计,也不能证明你的自定义 formatter 和构建配置已经安全。
实施步骤
- 1
把普通不可信文本放在固定字面量字段后。
- 2
真正需要模板时限制其长度和允许的功能。
- 3
审查队列与回调之间的参数所有权。
- 4
按输出目标编码,并明确处理截断。
可复制示例
#include <fmt/args.h>
#include <cassert>
#include <functional>
int main() {
char text[] = "abc";
fmt::dynamic_format_arg_store<fmt::format_context> store;
store.push_back(text);
store.push_back(std::cref(text));
store.push_back(fmt::string_view(text));
text[0] = 'X';
assert(fmt::vformat("{}|{}|{}", store) == "abc|Xbc|Xbc");
}常见问题
fmt 会自动保证生成的 HTML 安全吗?
不会。格式化与输出场景转义是两项职责,应使用合适的 HTML 编码或模板边界。
动态参数存储能让异步日志记录完全拥有数据吗?
不能自动保证。延迟操作之前,需要检查字符串视图、显式引用和自定义类型的存储要求。
资料来源
- doc/api.md来源核查 2026-09-08
- doc/syntax.md来源核查 2026-09-08
- include/fmt/core.h来源核查 2026-09-08
- include/fmt/args.h来源核查 2026-09-08
- test/format-test.cc来源核查 2026-09-08
- test/args-test.cc来源核查 2026-09-08