导出 CSV 时防止公式注入:Python 文本单元格防护与回读校验

举报
yd_246123679 发表于 2026/11/14 10:05:22 2026/11/14
【摘要】 针对导出 CSV 时用户可控文本被 Excel 当作公式执行的风险,给出用 Python csv.writer 对危险前缀单元格前置单引号、数字列保持原样的最小实现,并说明 Excel 打开核对与纯文本回读对比方法。

导出 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 消费端和所有注入载荷。
【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。