Web开发中常见的安全问题

SQL injection

漏洞简介

SQL注入:在参数处填写特殊字符以修改数据库查询命令功能,从而窃取或修改数据库信息。不安全的数据库调用方式容易引起此漏洞。

例如,利用字符串拼接书写SQL语句极易引起SQL注入。

1
2
# 一个十分初级的SQLi导致登录绕过的情况
query_str = "SELECT id FROM users WHERE name = '%s' and password = '%s'" % (username, password)

当用户填写信息导致username = "' or '1' = '1' /*"时,query_str 中实际的判断条件为:name = '' or '1' = '1', 显然为真,导致登录绕过。

SQLi 的危害不仅于此,发现此漏洞后可以通过添加分号后接各种语句达到遍历数据库甚至删库等操作。例如将前文中 name 填写为 ';DROP TABLE user;` 等。

规避方法

规避此漏洞最好的办法就是:不要自行组装查询语句,善用各种成熟的ORM框架,并禁止采用类似eval的函数。只要规范的使用OEM框架,SQLi就不会影响到日常开发。这是知识成本最低且最安全的方式。

XSS

漏洞简介

XSS即跨站脚本攻击,工作方式是在页面中嵌入恶意js代码,导致用户cookie等敏感信息泄露。可藉由表单填写、邮件、钓鱼网站等触发。实际工作时往往会携带用户cookie和网站域名请求一个攻击者预先布置好的服务器,导致攻击者直接窃取后台地址及登录权限,或邮箱登录权限等。

例如,在表单填写中填入:

1
<img src='' onerror='alert(document.cookie)' />

或者直接引入一个js文件,例如

1
&lt;script src='http://any_site/xss.js' /&gt;

规避方法

当前各种前端框架均提供有基本的xss防御手段,如

  • 将各种标签转换为&lt;实体字符
  • 在模板嵌入时采用text格式而不是html的格式。例如jquery插件等

但是具体使用时仍然需要测试,特别是某些二次展示或小型独立插件渲染的地方。比如将某段用户信息重新展示在弹出层,气泡框等地方的插件等,需要注意其是否有专门的参数控制渲染方式等。

另,开发之外,在平时使用中,尽量选用一个好用的邮件客户端。

CSRF

漏洞简介

CSRF 即 跨站请求伪造,其工作方式是通过骗取用户加载伪造的请求,向网站请求、POST数据以达成攻击目的。

例如,在A网站制作一个转账请求的链接诱导用户点击从而请求B银行网站接口

1
2
3
var xhr = new XMLHttpRequest();
xhr.open('POST', 'http://某银行/某转账操作/');
xhr.send('账户=我的账户&金额=好多好多钱');

若此用户正保存此银行网站cookie且银行网站无法防御CSRF攻击,则用户转账请求被伪造成功。

规避方法

CSRF通常危害各种有副作用的请求操作,例如提交表单等,主要在POST领域,所以通常也是防御POST行为。
这要求我们的POST操作不能仅仅信任cookie,还需要一些瞬时验证,比如在展示表单的时候在其中隐藏一次性token,在提交时验证并消耗此token。Django对此有标准的CSRF中间件。

另, 开发之外,生活中尽量使用现代浏览器,例如Chrome等,这些浏览器有严格的CORS机制防止跨站请求出现。

目录穿越

漏洞简介

此漏洞很少出现在现代框架中,其工作方式是在浏览器中尝试输入带有../的路径,去请求更上层的文件。例如尝试请求机器 /etc 目录下的某些关键配置等。

规避方法

除了在一些非常古老的php框架中外,此漏洞一般不会出现,但是也不要自己主动制造,比如不要在代码中尝试将用户输入直接拼接到文件打开路径中。

结语

一句话: Web开发中永远不要相信用户输入。

简单的带索引树构建

综述

日常开发中常会遇到有相互隶属关系的同种数据,例如用户权限,网站地图,文档目录,以及一些动态的分类方式等,甚至是pypi上的项目引用关系。此时相信大家都明白,应该使用的数据结构是树。

大家都知道使用树,那如何用得好,不至于走岔迷失呢?在我看来,重点一是在于方便寻址,根据业务需要,在创建树的同时创建好一个或多个适用的索引表;二是将树可视化、形象化,通过添加节点特征,规范变量名,或者建立可靠的序列化输出机制,从而便于程序判断和debug。 这两点非常有助于程序猿快速建立对这个树的直观映像,并提升代码的表达能力。

我相信,一个好的工程师应该拥有的素质,不是在复杂的数据结构中绕来绕去而不晕的能力,而是将一个看似复杂的数据结构快速梳理出线索,使其简单可靠的能力。而运用好之前提到的两点,就如同在几何题中做出了关键辅助线。

实践一 文档树

产品中有帮助系统,帮助文档的目录是一个简单的三层结构:产品,子目录,页面。

早年的目录结构是页面存在数据库中,子目录和产品的关系以字典结构直接写在代码中。后来又重构成了目录结构都以节点方式放在数据库,在代码中创建为树。
数据样例如下:

1
2
3
4
5
{"id": 0, "name": "some help menu"}
{"id": 1, "name": "yet a menu", "root": 0}
{"id": 2, "name": "menu x", "root": 1}
{"id": 3, "name": "another", "root": 0}
{"id": 4, "name": "some other product"}

但是这里有个问题,这段代码中每当需要使用到某一确定id的帮助文档时,总是在树中进行深度优先查找,嵌套多层循环。反向查找其父级内容创建面包屑时,又是在节点列表中不断循环查找其id和当前节点父级id相同的节点,直到父级id属性为空。
这种自找麻烦的情况在搜索帮助文档内容时达到峰值,页面需要一个新的缩小的树状结构用于展示搜索结果的分布情况并计数,代码中进行的判断和循环多达6次嵌套。请注意,也许有算法课教树的搜索是深度优先和广度优先等等。但是,我们使用时千万不要轻易在树中搜索。我们需要的是捷径。

我们来看看我们对此数据结构的需求是什么样的:

  • 页面上有个树形的帮助文档索引需要展示
  • 当显示某个确定的帮助文档时,需要展示面包屑导航栏
  • 对帮助进行搜索,展示时树中仅显示被命中的项目,并对每个分类被搜索到的项目计数

为了能实现这个简单的需求,我们需要对这个树添加一个索引。

首先,我们定义一个类,这个类的功能很简单,就是生成目录树,实例化完毕后,其tree属性就是一个描述树图的大字典,id_index属性则是一个id索引,另外,还在数据内部添加了一点标记。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
self.tree = {
'id_0': {
... # root content
menu_level: 0,
children: {
'menu_id': { ..., children: {...}}
'menu_id': { ..., children: {...}}
}
}
}
self.id_index = {
'id_0': root_data, # 即self.tree中root_
'id_1': data_1, # 即self.tree中children下的dict对象
'id_2': data_2,
'id_3': data_3,
...,
}

这里用到了python中对可变对象的引用或其他语言中的指针,这样的数据结构有个好处,就是可以根据id快速定位到某个帮助页面,同时根据获得的这个字典对象在树形图中快速寻找其下级目录,或者根据其root属性再次在index中定位其父级页面。

生成代码大致如下:

1
2
3
4
5
6
7
8
9
# 生成一个简单的树

class HelpMenuTree(object):

def __init__(self, node_list):
self.tree, self.id_index = self.__build(node_list)

def __build(self, node_list):
# 生成树的同时将每个节点放入到id_index中

此结构较为简易,没有在子节点的数据中留下指向父级的指针,仅保留了父级id,故可直接利用self.id_index查找父级,快速生成面包屑。

从这个例子,我们可以显而易见的发现索引的好处。同理可见于实践二。

实践二 权限树

我们产品后台的Django系统中虽然已利用原生权限系统,历来权限书写极为混乱,一个模块可能有几十个权限散落于各个页面,书写的权限名称也有随意性,与具体功能无法一一对应,亟待解决。经历某些事件,痛定思痛之后,决定重新创建一套权限系统。

首先,研究Django系统的特性之后,决定直接利用框架URL路由系统来作为权限系统的根基。先整理URL命名以吻合整个后台的页面层级,然后以路由树直接复刻出整个网站地图,从而可以通过这个树直观感受整个后台权限结构。我们在此创建出两个类,节点类及网络类,用以控制这整个树结构。创建树的逻辑很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class RouterNode(object):

def __init__(self, **kwargs):
self.url = ...
self.name = ...
self.is_leaf = True/False
self.perms = {
'get': [],
'post': [],
'put': [],
'patch': []
}
self.parent = ...
self.children = [...]


class NodeNetwork(object):

def __init__(self, router_data):
self.root_node = RouterNode('', root_node=True)
self.__url_map = self.__build_tree_by_routers(router_data)
self.__name_map = {
node.name: node for node in self.__path_tree.values()
}

def __build_tree_by_routers(self, router_data):
"""
根据url的分解目录等级,创建以self.root_node为根, 每个独立权限为叶子节点的树。
同时将url, name 分别建立索引
"""

def get_node_by_url(self, url):
return self.__url_map.get(url)

def get_node_by_name(self, name):
return self.__name_map.get(name)

def get_all_sub_node(self, url):
"""
根据非叶子节点获取所有叶子节点的值
"""

至此,我们同时可以在前端也用一个简单的递归把树创建到前端页面上,给予每个节点一个展开收起按钮,一个复选框,后台的权限管理是否立刻就清晰明了了呢~
同时,由于在树中添加了索引,根据url及权限名称寻找权限也是O(1)的操作,极为快速简洁,虽然是树的结构,一点也不影响操作速度。

综上

本文建议对业务中常见的对象间有较多关联关系的情况,直接在代码逻辑中将数据构建为多维度的关系映射,可同时构建为树,图,哈希map,队列等多重结构。可以同时利用多种数据结构的优势灵活编码,提高效率,且可令代码清晰明了。