如何正确避免StreamReader.ReadToEnd()引发的内存不足异常及DoS风险?
问题描述
我通过以下代码使用StreamReader.ReadToEnd()读取嵌入的文本文件:
private string GetCSSReset() { var assembly = typeof(EmailSender).GetTypeInfo().Assembly; using (var stream = EmbeddedScriptsProvider.ReadInternal(assembly, GlobalVars.CSSFileName)) { using (var textReader = new StreamReader(stream)) { var text = textReader.ReadToEnd(); return $"{text}"; } } }
但Fortify SCA扫描指出这段代码可能引发拒绝服务(DoS)攻击。我已经修改了EmbeddedScriptsProvider.ReadInternal()方法来检查流的长度:
internal static long MBtoContentLength(int megaByte) { return megaByte * 1024 * 1024; } internal static Stream? ReadInternal(Assembly assembly, string resource) { var stream = assembly.GetManifestResourceStream($"{assembly.GetName().Name}.{resource}"); if(stream.Length > MBtoContentLength(GlobalVars.MAX_FILE_SIZE_MB)) { throw new InvalidOperationException("File size too large."); } return stream; }
请问这种处理方式是否正确?我见过使用.Read()并检查字符串长度的方法,该如何将其应用到ReadToEnd()场景中?
你的处理方式是否正确?
这种处理方式是有效的,但有几个细节需要完善:
- 对于嵌入资源而言,
stream.Length是可靠的——嵌入资源的大小在编译时就已固定,不会在运行时被篡改,提前检查流长度可以避免ReadToEnd()一次性加载过大内容到内存,从根源上防止内存耗尽引发的DoS。 - 要确保
GlobalVars.MAX_FILE_SIZE_MB的取值合理:既要覆盖正常业务所需的最大文件尺寸,又要限制在不会造成内存压力的范围内。 - 需补充空值判断:如果
GetManifestResourceStream返回null(比如资源不存在),直接访问stream.Length会抛出NullReferenceException,建议先判断stream是否为null,再执行长度检查。
如何将.Read()检查长度的方法应用到ReadToEnd()场景?
如果希望通过分段读取(用Read())来控制内存占用,替代一次性ReadToEnd(),可以按以下方式实现:
private string GetCSSReset() { var assembly = typeof(EmailSender).GetTypeInfo().Assembly; using (var stream = EmbeddedScriptsProvider.ReadInternal(assembly, GlobalVars.CSSFileName)) { if (stream == null) { throw new InvalidOperationException("Resource not found."); } using (var textReader = new StreamReader(stream)) { // 按字符数估算上限(需根据编码调整,此处为简化处理) var maxChars = (int)(MBtoContentLength(GlobalVars.MAX_FILE_SIZE_MB) / sizeof(char)); var sb = new StringBuilder(); char[] buffer = new char[4096]; // 4KB缓冲区,可根据实际调整 int readChars; while ((readChars = textReader.Read(buffer, 0, buffer.Length)) > 0) { if (sb.Length + readChars > maxChars) { throw new InvalidOperationException("File size exceeds allowed limit."); } sb.Append(buffer, 0, readChars); } return sb.ToString(); } } }
这种方式的优势:
- 不会一次性将整个文件加载到内存,而是分块读取,更适合处理超大文件(不过对于嵌入资源来说,提前检查流长度已经足够,这种方式更多用于动态加载的外部文件)。
- 实时检查已读取的字符数,一旦超过上限就抛出异常,避免内存溢出。
需要注意:
- 字符数和字节数的转换要考虑编码(比如UTF-8中一个字符可能占1-4字节),上面的估算为简化处理,若要精确控制,可先通过
stream.Read读取字节再转换为字符串,但逻辑会更复杂。 - 缓冲区大小可根据实际情况调整:过大的缓冲区会失去分段读取的意义,过小则会增加IO次数。
内容的提问来源于stack exchange,提问作者Lee
相关产品推荐
相关产品推荐

