导出 CSV 时防止公式注入:Python 文本单元格防护与回读校验
导出 CSV 时防止公式注入:Python 文本单元格防护与回读校验
问题场景
系统导出 CSV 后,用户常用 Excel 打开。如果单元格内容来自用户输入,且以 =、+、-、@、制表符或回车开头,Excel 类工具可能把它当作公式解析。攻击者可以构造 =SUM(...)、+cmd 等内容,诱导打开文件的人执行非预期计算或交互。
需要说明的是,这不是 CSV 语法错误,而是表格软件对单元格内容的解释问题。防护目标不是修改所有字段,而是明确区分用户可控文本列与程序生成的数字列:文本列加安全前缀,数字列保持原样,最终仍由 csv.writer 统一写出。
防护策略
- 明确列类型:调用方必须指出哪些列是用户可控文本列,哪些列是由程序生成且类型可信的数字列。
- 文本列若首字符属于危险前缀集合,则前置单引号
',让表格软件按文本处理。 - 数字列不参与前缀处理,保持原值写出;如果数字列本身也来自用户输入,应把它归入文本列。
- 字段内的逗号、双引号、换行交给
csv.writer转义,避免手工拼接 CSV。 - 纯文本回读时,去掉本函数添加的安全前缀后再与原始文本比较,便于自动化测试。
最小可运行代码
import csv
from pathlib import Path
DANGEROUS_PREFIXES = ("=", "+", "-", "@", "\t", "\r")
def safe_text_cell(value):
if not isinstance(value, str):
return value
if value.startswith(DANGEROUS_PREFIXES):
return "'" + value
return value
def write_csv_safely(rows, output_path, text_columns):
with open(output_path, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f, quoting=csv.QUOTE_MINIMAL)
for row in rows:
safe_row = []
for idx, cell in enumerate(row):
if idx in text_columns:
safe_row.append(safe_text_cell(cell))
else:
safe_row.append(cell)
writer.writerow(safe_row)
def read_csv_rows(path):
with open(path, newline="", encoding="utf-8-sig") as f:
return list(csv.reader(f))
def strip_guard_prefix(value):
if (isinstance(value, str) and value.startswith("'")
and len(value) > 1 and value[1] in DANGEROUS_PREFIXES):
return value[1:]
return value
def assert_text_columns_roundtrip(original_rows, read_rows, text_columns):
assert len(original_rows) == len(read_rows)
for original, back in zip(original_rows, read_rows):
for idx in text_columns:
assert strip_guard_prefix(back[idx]) == original[idx]
if __name__ == "__main__":
rows = [
["name", "amount", "note"],
["张三", 12.5, "=SUM(A1:A2)"],
["李四", 3, "@cmd"],
["王五", 7, "+1"],
]
text_columns = {0, 2}
output = Path("safe_export.csv")
write_csv_safely(rows, output, text_columns)
back = read_csv_rows(output)
assert_text_columns_roundtrip(rows, back, text_columns)
print("written:", output)
print("read back:", back)
示例中 amount 是数字列,保持原样;name 和 note 是文本列,其中 note 的危险前缀会被加上单引号。utf-8-sig 便于 Excel 识别 UTF-8 文本,但编码选择仍应服从实际下游系统。
运行方法
将代码保存为 guard_csv.py,执行 python guard_csv.py。程序会生成 safe_export.csv,并打印 csv.reader 回读后的二维列表。断言通过表示指定文本列在去除安全前缀后与原始值一致。
如果要把函数接入业务导出流程,只需把当前行的用户可控文本列索引传入 text_columns,并确保数字列在写出前已经是可信的数值类型。
Excel 打开验证
用 Excel 打开生成的 safe_export.csv,选中 note 列对应单元格,通过编辑栏核对内容是否仍作为文本存在,而不是出现公式计算结果或 #NAME? 等错误。该步骤是人工检查方式,不应视为对任意电子表格软件的完整安全测试。
纯文本回读对比
read_csv_rows 使用 csv.reader 读回文件,strip_guard_prefix 只去掉“本函数添加的单引号前缀”,即单引号后紧跟危险字符的情况。这样既能验证防护前缀可逆,也能避免误删业务原本以单引号开头的普通文本。
边界与限制
- 当前只处理字符串;
None、整数、浮点数原样写出。 - 只对调用方指定的文本列处理。列归属必须由业务明确,不能靠“看起来像数字”判断。数字列如果来自用户输入,应按文本列处理。
- 只覆盖以
=、+、-、@、制表符、回车直接开头的情况。前导空格后再跟公式符等变体,需要业务侧先规范化或扩展规则。 - 单引号前缀会保留在纯文本 CSV 中。下游按原始文本比较时需去除前缀;不同表格软件对前缀的显示方式可能不同。
csv.writer负责字段内逗号、双引号、换行的语法转义,不负责业务层的公式注入策略。- 该策略降低 Excel 类工具误执行公式的风险,但不承诺覆盖所有 CSV 消费端和所有注入载荷。
- 点赞
- 收藏
- 关注作者
评论(0)