安全编码&代码坏味道
安全编码&代码坏味道
安全编码
引言
-
安全编码之基本思想
-
程序在处理外部数据时必须经过严格的合法性校验
-
禁用不用的端口,尽量减少代码的攻击面
-
编码在一定范围内应该对不信任模块间采取防御式编程,以此来弥补潜在的人工疏忽
-
HCSEC黄金规则
-
多所有的外部数据进行边界和格式检查
-
对于内部数据需要划分信任域进行检查
-
在形成系统执行命令前经过充分净化
-
只有线程安全的代码才能应用在多线程中
-
所有STL只支持同时读,只要有一个线程发生写操作,其他线程都可能存在问题
-
尽量减少全局变量、静态变量或单例的使用,控制共享的变量的作用域
-
-
避免重复代码
-
废弃代码存在安全隐患
- 未编译、未链接的代码,以及注释的代码的安全问题仍然是安全问题
安全编程规范
-
字符串与数组操作
-
确保有足够的存储空间,防止缓冲区溢出
-
调用格式化函数时,禁止format参数由外部可控,否则可能造成字符串格式化漏洞
-
若在调用printf时没有传入参数列表,那么printf会按照format格式去栈中取对应的参数(地址偏移)
-
可以利用编译器检查格式化参数类型与实际参数的匹配性:
add_definitions(-DSECUREC_SUPPORT_FORMAT_WARNING=1)
-
-
-
正确使用安全函数
-
安全函数增强了哪些安全特性?
-
强化边界检查:在接口参数中增加buffer长度的参数
-
保证字符串以'\0'结尾,防止防伪buffer边界之外的信息
-
发生缓冲区溢出时,将目的缓冲区的首字节置为0
-
-
正确设置安全函数中的destMax参数(包含结束符)
-
数组或指针作为函数参数时,必须同时将其长度作为函数的参数
-
常见错误
-
使用宏、常量或魔鬼数字作为destMax
-
参数是数组,使用形参中数组的长度
- 形参看上去数组有长度,但实际上会退化为指针
-
使用宏或函数对安全函数进行封装
-
-
必须检查安全函数的返回值,并进行正确的处理
-
-
整数
-
注意溢出
-
除法和取模时保证除数不为0
-
-
内存
-
在做内存操作时,应该做好严格的边界校验
-
空指针、已经释放的野指针
-
尽量使用引用、智能指针,而非指针
-
-
文件操作
-
不规范的文件路径,导致文件泄露
- 使用realpath函数对路径进行规范化,之后再对路径进行校验
-
打开文件后不关闭,导致文件句柄耗尽而无法打开文件
- 尽量使用守护类、自定义析构的unique_ptr
-
代码坏味道
- 代码坏味道: 《重构 改善既有代码的设计》By Martin Fowler
一、 代码坏味道概述
-
代码坏味道不是功能性问题,主要表现为 可读性 和 可维护性 的一些问题
-
常见的代码坏味道: 冗余和重复 、 局部膨胀 、 耦合结构不良
-
不同层次的坏味道
-
直观: 一般为规范性问题,可以通过代码扫描工具识别。
-
微观: 主要为软件细节设计的问题,一般比较具体明确,可以通过查看代码进行识别。
-
宏观: 代码架构上的整体的问题,主要为软件高系统设计的问题,一般比较主观抽象,需要结合业务领域知识进行识别。 例:类职责不单一
-
-
不同类别的坏味道
-
膨胀剂: 太长的方法、太大的类、太多参数、基本类型偏执、数据泥团
-
滥用OO:switch语句(可以用多态)、临时字段、被拒绝的馈赠(为避免组合而使用继承)、异曲同工的类
-
难以修改: 发散式修改(类/函数承载了过多功能)、霰弹式修改(有一处修改,其他地方也需要修改)
-
可有可无:注释、重复代码、冗赘类/元素、数据类、死代码、夸夸其谈未来性
-
耦合: 依恋情结(需要调用一堆接口来实现一个功能)、内幕交易、消息链、中间人
-
神秘命名、全局数据、可变数据、循环语句
-
二、 冗余和重复
-
重复代码 Duplicated Code
-
多个地点有同样的程序片段
-
万恶之源 一旦变化,到处修改,漏改一处就是bug
-
解决方案
-
同一个类的两个函数有重复代码: 提取
-
互为兄弟的类有重复代码: 移到父类
-
互为兄弟的类有相似代码: 在父类创建模板方法,差异部分交给子类实现
-
毫不相关的类出现重复: 先提炼到一个类里,然后在另一个类里使用
-
-
CAUTION 过多消除重复可能会增强耦合
-
-
过多注释 Comments
-
冗余注释之所以存在,是因为代码很糟糕
-
破坏可读性,有些误导性的注释让维护人员陷入困境
-
解决方案
-
用来解释一段代码用来做什么时,把它提取成函数,修改函数名
-
注释why,而不仅是how和what
-
-
-
夸夸其谈未来性 Speculative Generality
-
过度关注未来可能的变化,增加了一些不必要的东西
-
滥用设计模式,导致难以找到主要的逻辑走向
-
过度的设计导致代码不易理解、错误不易定位,可能还会降低代码执行的效率
-
放置过量的callback或特殊情况来处理一些非必要的事情
-
解决方案
-
如果某个抽象类没有太大作用,使用折叠层次Collapse Hierarchy删除抽象类
-
非必要的delegation可以使用内联类Inline Class代替
-
函数的某些参数未被使用,可以实施Remove Parameter
-
函数名称带有多余的意味,应该实施Rename Method让他现实一点
-
-
解决方案
-
太多参数:把多个参数用类封装
-
switch: map + find
-
循环查找: find + lambda
三、 局部膨胀
-
过长参数列表 Long Parameter List
-
太长参数列表难以理解
-
解决方案
-
一个参数可以通过另一个参数查询时,使用以查询取代参数Replace Parameter with Query
-
多个参数属于同一个数据结构时,直接传入数据结构的对象以保持对象完整Preserve Whole Object
-
多个参数有关联,总是同时使用,可以引入参数对象Introduce Parameter Object
-
某个参数是标记用于区分函数行为的,移除标记参数Remove Flag Argument
-
多个函数有相同的参数,实际上是围绕这些参数工作,可以将多个函数组合成类Combine Functions into Class
-
-
注意全局变量、静态变量等隐形传入的“参数”
-
常常同时存在 过长函数、数据泥团、基本类型偏执 等其他坏味道,需要一并消除
-
四、 耦合结构不良
-
发散式变化Divergent Change
-
某个模块因为不同的原因在不同的方向上变化
-
职责过多,理解困难
-
解决方案
-
按不同变化方向进行拆分
-
如果功能按照某种步骤进行,可以使用拆分阶段Split Phase将不同的阶段分开
-
-
模块的职责是否单一根据所在架构层次的不同而不同
-
-
霰弹式修改Shotgun Surgery
-
代码维护时多处修改,容易遗漏
-
解决方案
-
将功能集中到一起,常用到搬移函数Move Function、搬移字段Move Field、搬移语句到函数Move Statements into Function 等搬移特性的重构手法,集中到负责的一个模块
-
如果功能本身在架构层次上不应该分开,可以使用内联函数Inline Function、内联类Inline Class
-
-
-
重复switch Repeated Switches
-
解决方案
-
面向对象的语言: 以多态取代条件表达式
-
面向过程的语言: 表驱动、策略模式
-
-
不是所有switch都需要重构,以下情况需要重构
-
类型码不断增加
-
单个case处理多件事
-
相同的switch语句分散在多处代码中
-
-
遵循单一职责原则
-
减少N:1:N的调用
-
-
临时字段Temporary Field
-
某个变量仅为某种特定场景而设,或只在该对象某一段生命周期内生效
-
不易被理解,可能会被误用
-
解决方案
-
Extract Class
-
作为参数传递
-
-
五、 代码坏味道工程工具
-
静态检查工具Xlint、Clang、FindBugs、CheckStyle、SourceMonitor,结合IDE使用,可以在写代码时识别规范性代码坏味道
-
CodeDEX提交代码时识别规范性或更高层次的坏味道
-
代码度量工具
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!