拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CsvHelper 写入指南:从安全注入防护到多种对象类型的 CSV 序列化

CsvHelper 写入指南:从安全注入防护到多种对象类型的 CSV 序列化
  • 后端
  • 数据工程

【免费下载链接】CsvHelper

Library to help reading and writing CSV files

项目地址:https://gitcode.com/gh_mirrors/cs/CsvHelper
点击查看免费下载

导读

本指南基于 CsvHelper 官方示例文档(writing/index.md)撰写,聚焦 CsvWriter 的完整使用路径:如何在写入前防范 CSV 注入攻击、如何将类对象/动态对象/匿名类型对象批量写出为 CSV,以及如何在已有文件上追加记录而不重复输出表头。读完本文,你将掌握InjectionOptions四种防护策略的取舍、WriteRecords的底层写入流程,以及面向不同对象类型的四种实战写入方案。

写入前的安全防线:CSV 注入与 InjectionOptions

当 CSV 文件被 Excel、Google Sheets 等外部程序打开时,字段中如果以=,@,+等字符开头,可能被当作公式执行,从而产生注入漏洞(即经典的 CSV Injection 攻击)。CsvHelper 为此在写入侧提供了可配置的注入防护机制,核心配置项均定义在 CsvConfiguration.cs:

配置项默认值说明
InjectionCharacters['=', '@', '+', '-', '\t', '\r']需要检测的注入字符集合
InjectionEscapeCharacter'(单引号)Escape 模式下前置的转义字符
InjectionOptionsNone注入防护策略

注入字符可以是字段的第一个字符,也可以是带引号字段的第一个字符,即=foo和"=foo"都在检测范围内。检测逻辑与上述默认值在源码 CsvWriter.cs 中完成初始化,真正的判定与处理则由SanitizeForInjection方法在字段写入前执行。

四种防护策略的语义与源码验证

InjectionOptions枚举定义于 InjectionOptions.cs,包含四个取值:

  • None(默认):不做任何注入防护,写入行为与未开启配置时完全一致。
  • Exception:一旦检测到注入字符,直接抛出WriterException,中断写入。源码实现在 CsvWriter.cs 中:throw new WriterException(context, $"Injection character '{field[injectionCharIndex]}' detected")。
  • Strip:移除字段开头的所有注入字符。===foo会被剥成foo。源码 CsvWriter.cs 中通过循环Substring(1)直到开头不再命中注入字符;若原本是带引号字段,则保留引号框架后仅剥离其中的注入前缀。
  • Escape:检测到注入字符时,在字段前补上InjectionEscapeCharacter(默认'),并确保字段被引号包裹。转换示例(与官方文档一致,均由源码 CsvWriter.cs 的分支逻辑产生):
=one -> "'=one" "=one" -> "'=one" =one"two -> "'=one""two"

上表中第三个示例展示了引号转义规则:字段内的引号会按 CSV 规则翻倍("变为""),从而保证转义后的字段依然是一个合法 CSV 字段。

这些行为在测试套件 SanitizationTests.cs 中均有对应验证,例如WriteField_NoQuotes_OptionsException_ThrowsException(无引号字段 + Exception 模式抛异常)、WriteField_NoQuotes_MultipleChars_OptionsStrip_StripsCharacter(多字符前缀剥离)以及WriteField_NoQuotes_OptionsEscape_QuotesFieldAndEscapes(Escape 模式的引号包裹与转义),可以直接运行这些用例来确认各种边界行为。

默认关闭的原因与启用建议

Escape 选项默认关闭(即InjectionOptions默认是None)。正如官方文档强调的:本库的首要目标是读写 CSV,而不是替用户过滤数据。但如果满足以下两个条件,强烈建议启用:

  1. 你存储的是用户输入且未经自行消毒的数据;
  2. 这些 CSV 文件可能被其他人用 Excel / Sheets 等程序打开。

另外需要注意:InjectionEscapeCharacter在读取时不会被移除。也就是说,如果你用 CsvWriter 的 Escape 策略写出'=one,之后再用 CsvReader 读回,得到的字段值仍然是'=one而不是=one。这一点在设计读写对称性时必须留意。

开启注入防护的两种配置方式

注入相关配置属于CsvConfiguration,因此既可以用CsvConfiguration对象集中配置,也可以通过特性(Attribute)在模型类上声明。

方式一:通过 CsvConfiguration 对象配置

var config = new CsvConfiguration(CultureInfo.InvariantCulture) { InjectionOptions = InjectionOptions.Escape, // 自定义注入字符集合(可选,默认已包含 = @ + - \t \r) // InjectionCharacters = new[] { '=', '@', '+', '-', '\t', '\r' }, // 自定义转义字符(可选,默认是单引号) // InjectionEscapeCharacter = '\'', }; using (var writer = new StreamWriter("path\\to\\file.csv")) using (var csv = new CsvWriter(writer, config)) { csv.WriteRecords(records); }

方式二:通过 Attribute 在模型上声明

CsvHelper 的配置体系支持“大部分类映射配置都能用特性完成”,InjectionOptionsAttribute.cs、InjectionCharactersAttribute.cs、InjectionEscapeCharacterAttribute.cs 三个特性与上述配置项一一对应,可在实体类上直接标注:

[InjectionOptions(InjectionOptions.Escape)] public class Foo { public int Id { get; set; } public string Name { get; set; } }

关于特性(Attributes)与类映射(Class Maps)配置的整体对比,可参考配置特性文档。

使用 CsvWriter 写出类对象(Class Objects)

最常见的写入场景是批量写出强类型类对象。官方示例(write-class-objects/index.md)如下:

void Main() { var records = new List<Foo> { new Foo { Id = 1, Name = "one" }, }; using (var writer = new StreamWriter("path\\to\\file.csv")) using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) { csv.WriteRecords(records); } } public class Foo { public int Id { get; set; } public string Name { get; set; } }

输出结果:

Id,Name 1,one

关键点:

  • CsvWriter接收TextWriter(这里是StreamWriter),配合using保证刷新与释放;
  • CultureInfo.InvariantCulture用于统一数字与日期的格式化,避免不同系统区域设置导致输出不一致;
  • 表头(Id,Name)由成员名自动推导并写在首行。

从源码看,WriteRecords(IEnumerable)的实现位于 CsvWriter.cs:先对首个记录调用WriteHeaderFromRecord写出表头,再对每个记录通过记录管理器获取对应的写入委托逐个写出,最后调用NextRecord换行。空集合会直接返回,不会输出任何内容。

使用 CsvWriter 写出动态对象(Dynamic Objects)

当字段在运行时才确定(例如来自配置、动态数据源或反序列化结果)时,可以写出ExpandoObject等动态对象。官方示例(write-dynamic-objects/index.md):

void Main() { var records = new List<dynamic>(); dynamic record = new ExpandoObject(); record.Id = 1; record.Name = "one"; records.Add(record); using (var writer = new StringWriter()) using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) { csv.WriteRecords(records); writer.ToString().Dump(); } }

输出结果:

Id,Name 1,one

要点:

  • 这里改用StringWriter,方便在内存中直接观察输出(示例中的Dump()是 LINQPad 的调试输出方法);
  • 动态对象的属性名来自ExpandoObject的键,写入顺序按添加顺序输出;
  • 当WriteRecords收到非泛型IEnumerable(List<dynamic>编译为IEnumerable<object>)时,运行时会对每个记录的实际类型进行判断——这正是非泛型重载比泛型重载更灵活的地方:集合中允许出现不同类型,逐个按真实类型写入,源码见 CsvWriter.cs 中对record.GetType()的动态分派。

使用 CsvWriter 写出匿名类型对象(Anonymous Type Objects)

匿名类型非常适合一次性导出场景——无需定义实体类即可导出。官方示例(write-anonymous-type-objects/index.md):

void Main() { var records = new List<object> { new { Id = 1, Name = "one" }, }; using (var writer = new StreamWriter("path\\to\\file.csv")) using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) { csv.WriteRecords(records); } }

输出结果:

Id,Name 1,one

要点:

  • 匿名类型对象同样走非泛型WriteRecords(IEnumerable)路径,集合声明为List<object>,实际写入时按匿名类型的真实类型分派;
  • 由于GetTypeInfoForRecord<T>对typeof(T) == typeof(object)的情况会取记录的真实类型(CsvWriter.cs),匿名类型的成员名和值都能被正确序列化;
  • 匿名类型适合临时导出,若字段需要复用或加配置,建议仍使用类对象方案。

向已有 CSV 文件追加数据

追加写入的核心是:不要重复输出表头。官方示例(appending-to-an-existing-file/index.md)分两步演示:

void Main() { var records = new List<Foo> { new Foo { Id = 1, Name = "one" }, }; // 1. 第一次写入,输出文件并带表头 using (var writer = new StreamWriter("path\\to\\file.csv")) using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) { csv.WriteRecords(records); } records = new List<Foo> { new Foo { Id = 2, Name = "two" }, }; // 2. 追加写入,不再输出表头 var config = new CsvConfiguration(CultureInfo.InvariantCulture) { // 关键:禁止再次写出表头 HasHeaderRecord = false, }; using (var stream = File.Open("path\\to\\file.csv", FileMode.Append)) using (var writer = new StreamWriter(stream)) using (var csv = new CsvWriter(writer, config)) { csv.WriteRecords(records); } } public class Foo { public int Id { get; set; } public string Name { get; set; } }

最终文件内容:

Id,Name 1,one 2,two

实现要点:

  • 追加必须用FileMode.Append打开文件流,避免覆盖原文件;
  • HasHeaderRecord = false让第二次WriteRecords跳过表头写入(即WriteHeaderFromRecord不再输出表头行);
  • 追加的数据需与原文件保持相同的字段顺序与数量,否则列会错位。

小结与进阶指引

CsvHelper 的写入能力可以概括为一条主线:配置CsvConfiguration→ 包装TextWriter→ 构造CsvWriter→ 调用WriteRecords。无论对象是强类型类、动态对象还是匿名类型,写入流程与输出格式保持一致;而注入防护(InjectionOptions)则是写入侧唯一需要主动决策的安全配置。

如需继续深入,可在当前仓库中查看以下资源:

  • 写入底层实现:CsvWriter.cs(WriteRecords系列重载位于第 350 行起,注入清洗逻辑SanitizeForInjection位于第 735 行)
  • 注入配置定义:CsvConfiguration.cs、InjectionOptions.cs
  • 注入防护测试:SanitizationTests.cs
  • 更多写入示例:目录 writing 下的四个示例页,以及 docs/examples/writing 对应的生成版 HTML 文档
  • 后端
  • 数据工程

【免费下载链接】CsvHelper

Library to help reading and writing CSV files

项目地址:https://gitcode.com/gh_mirrors/cs/CsvHelper
点击查看免费下载

相关推荐

上一篇:终极指南:OP自动化插件的完整使用教程
下一篇:3步搞定Tasmota固件刷写:新手零安装刷机完整教程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

返回列表