常见的Web攻击手段之CSRF攻击

安全|常见的Web攻击手段之CSRF攻击(转载)

转载自简书

对于常规的Web攻击手段,如XSS、CRSF、SQL注入、(常规的不包括文件上传漏洞、DDoS攻击)等,防范措施相对来说比较容易,对症下药即可,比如XSS的防范需要转义掉输入的尖括号,防止CRSF攻击需要将cookie设置为httponly,以及增加session相关的Hash token码 ,SQL注入的防范需要将分号等字符转义,等等做起来虽然筒单,但却容易被忽视,更多的是需要从开发流程上来予以保障(这句话是给技术管理者的建议),以免因人为的疏忽而造成损失。

一、CSRF介绍

CSRF攻击的全称是跨站请求伪造( cross site request forgery),是一种对网站的恶意利用,尽管听起来跟XSS跨站脚本攻击有点相似,但事实上CSRF与XSS差别很大,XSS利用的是站点内的信任用户,而CSRF则是通过伪装来自受信任用户的请求来利用受信任的网站。你可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义向第三方网站发送恶意请求。 CRSF能做的事情包括利用你的身份发邮件、发短信、进行交易转账等,甚至盗取你的账号。

1.1、CRSF攻击原理

CRSF攻击原理

  1. 首先用户C浏览并登录了受信任站点A;
  2. 登录信息验证通过以后,站点A会在返回给浏览器的信息中带上已登录的cookie,cookie信息会在浏览器端保存一定时间(根据服务端设置而定);
  3. 完成这一步以后,用户在没有登出(清除站点A的cookie)站点A的情况下,访问恶意站点B;
  4. 这时恶意站点 B的某个页面向站点A发起请求,而这个请求会带上浏览器端所保存的站点A的cookie;
  5. 站点A根据请求所带的cookie,判断此请求为用户C所发送的。

因此,站点A会报据用户C的权限来处理恶意站点B所发起的请求,而这个请求可能以用户C的身份发送 邮件、短信、消息,以及进行转账支付等操作,这样恶意站点B就达到了伪造用户C请求站点 A的目的。
受害者只需要做下面两件事情,攻击者就能够完成CSRF攻击:

  • 登录受信任站点 A,并在本地生成cookie;
  • 在不登出站点A(清除站点A的cookie)的情况下,访问恶意站点B。

很多情况下所谓的恶意站点,很有可能是一个存在其他漏洞(如XSS)的受信任且被很多人访问的站点,这样,普通用户可能在不知不觉中便成为了受害者。

1.2、攻击举例

假设某银行网站A以GET请求来发起转账操作,转账的地址为www.xxx.com/transfer.do?accountNum=l000l&money=10000,参数accountNum表示转账的账户,参数money表示转账金额。
而某大型论坛B上,一个恶意用户上传了一张图片,而图片的地址栏中填的并不是图片的地址,而是前而所说的砖账地址:<img src="http://www.xxx.com/transfer.do?accountNum=l000l&money=10000">
当你登录网站A后,没有及时登出,这时你访问了论坛B,不幸的事情发生了,你会发现你的账号里面少了10000块...
为什么会这样呢,在你登录银行A时,你的浏览器端会生成银行A的cookie,而当你访问论坛B的时候,页面上的标签需要浏览器发起一个新的HTTP请求,以获得图片资源,当浏览器发起请求时,请求的却是银行A的转账地址www.xxx.com/transfer.do?accountNum=l000l&money=10000,并且会带上银行A的cookie信息,结果银行的服务器收到这个请求后,会以为是你发起的一次转账操作,因此你的账号里边便少了10000块。
当然,绝大多数网站都不会使用GET请求来进行数据更新,因此,攻击者也需要改变思路,与时俱进。
假设银行将其转账方式改成POST提交,而论坛B恰好又存在一个XSS漏洞,恶意用户在它的页面上植入如下代码:

1
2
3
4
5
6
7
8
<form id="aaa" action="http://www.xxx.com/transfer.do" metdod="POST" display="none">
<input type="text" name="accountNum" value="10001"/>
<input type="text" name="money" value="10000"/>
</form>
<script>
var form = document.forms('aaa');
form.submit();
</script>

如果你此时恰好登录了银行A,且没有登出,当你打开上述页面后,脚本会将表单aaa提交,把accountNum和money参数传递给银行的转账地址http://www.xxx.com/transfer.do,同样的,银行以为是你发起的一次转账会从你的账户中扣除10000块。
当然,以上只是举例,正常来说银行的交易付款会有USB key、验证码、登录密码和支付密码等一系列屏障,流程比上述流程复杂得多,因此安全系数也高得多。

1.3、CSRF的防御

1、尽量使用POST,限制GET
GET接口太容易被拿来做CSRF攻击,看上面示例就知道,只要构造一个img标签,而img标签又是不能过滤的数据。接口最好限制为POST使用,GET则无效,降低攻击风险。
当然POST并不是万无一失,攻击者只要构造一个form就可以,但需要在第三方页面做,这样就增加暴露的可能性。
2、将cookie设置为HttpOnly
CRSF攻击很大程度上是利用了浏览器的cookie,为了防止站内的XSS漏洞盗取cookie,需要在cookie中设置“HttpOnly”属性,这样通过程序(如JavaScript脚本、Applet等)就无法读取到cookie信息,避免了攻击者伪造cookie的情况出现。
在Java的Servlet的API中设置cookie为HttpOnly的代码如下:
response.setHeader( "Set-Cookie", "cookiename=cookievalue;HttpOnly");
3、增加token
CSRF攻击之所以能够成功,是因为攻击者可以伪造用户的请求,该请求中所有的用户验证信息都存在于cookie中,因此攻击者可以在不知道用户验证信息的情况下直接利用用户的cookie来通过安全验证。由此可知,抵御CSRF攻击的关键在于:在请求中放入攻击者所不能伪造的信息,并且该信总不存在于cookie之中。鉴于此,系统开发人员可以在HTTP请求中以参数的形式加入一个随机产生的token,并在服务端进行token校验,如果请求中没有token或者token内容不正确,则认为是CSRF攻击而拒绝该请求。
假设请求通过POST方式提交,则可以在相应的表单中增加一个隐藏域:
<input type="hidden" name="_toicen" value="tokenvalue"/>
token的值通过服务端生成,表单提交后token的值通过POST请求与参数一同带到服务端,每次会话可以使用相同的token,会话过期,则token失效,攻击者因无法获取到token,也就无法伪造请求。
在session中添加token的实现代码:

1
2
3
4
5
HttpSession session = request.getSession();
Object token = session.getAttribute("_token");
if(token == null I I "".equals(token)) {
session.setAttribute("_token", UUID.randomUUIDO .toString());
}

4、通过Referer识别
根据HTTP协议,在HTTP头中有一个字段叫Referer,它记录了该HTTP请求的来源地址。在通常情况下,访问一个安全受限的页面的请求都来自于同一个网站。比如某银行的转账是通过用户访问http://www.xxx.com/transfer.do页面完成的,用户必须先登录www.xxx.com,然后通过单击页面上的提交按钮来触发转账事件。当用户提交请求时,该转账请求的Referer值就会是
提交按钮所在页面的URL(本例为www.xxx. com/transfer.do)。如果攻击者要对银行网站实施CSRF攻击,他只能在其他网站构造请求,当用户通过其他网站发送请求到银行时,该请求的Referer的值是其他网站的地址,而不是银行转账页面的地址。因此,要防御CSRF攻击,银行网站只需要对于每一个转账请求验证其Referer值即可,如果是以www.xx.om域名开头的地址,则说明该请求是来自银行网站自己的请求,是合法的;如果Referer是其他网站,就有可能是CSRF攻击,则拒绝该请求。
取得HTTP请求Referer:
String referer = request.getHeader("Referer");

二、总结

CSRF攻击是攻击者利用用户的身份操作用户帐户的一种攻击方式,通常使用Anti CSRF Token来防御CSRF攻击,同时要注意Token的保密性和随机性。
并且CSRF攻击问题一般是由服务端解决。
注:文章大部分内容来源于《大型分布式网站架构 设计与实践》一书。

pyenv python2.7/3.6 共存/切换实践

在docker(python27) 环境中

参考 简书-pyenv 让 python 版本完美切换

更多见github/pyenv-virtualenv

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
docker search python
>> ...

docker pull centos/python-27-centos7

docker images

<!--运行docker 环境 -->
docker run -i -t centos/python-27-centos7 /bin/bash

<!--继续配置环境... -->

<!--首先把项目克隆下来,放在家目录下的隐藏文件夹中:.pyenv-->
git clone https://github.com/pyenv/pyenv.git ~/.pyenv

<!--配置环境变量 ,依次执行如下命令-->
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
echo -e 'if command -v pyenv 1>/dev/null 2>&1; then\n eval "$(pyenv init -)"\nfi' >> ~/.bashrc


<!--安装 pyenv-virtualenv-->
<!--插件克隆在刚才已经安装完毕的 pyenv 的 plugins 文件夹中-->
git clone https://github.com/pyenv/pyenv-virtualenv.git $(pyenv root)/plugins/pyenv-virtualenv
echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.bashrc

source ~/.bashrc

<!--开始使用 pyenv-->

pyenv version/versions(查看本地安装的python版本)

<!-- pyenv install 敲tag ,可列出支持的版本 -->
pyenv install 3.6.8

<!--如果失败,可安装依赖的包-->
sudo yum install gcc zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel openssl-devel tk-devel libffi-devel

pyenv versions

<!--pyenv 托管 python 版本,virtualenv 使用 python 版本-->

<!--创建虚拟环境-->
pyenv virtualenv 3.6.8 venv3.6.8

<!--激活, 在docker中,发现activate 并不能成功切换环境 ,最终使用 local 可以完成切换 -->
<!--local 命令,会在目标文件夹中生成 .python-version 文件, 文件内容是 venv3.6.8, 这样只要进入目录就会自动激活环境 -->
pyenv activate venv3.6.8
pyenv local venv3.6.8

<!--查看pip 安装目录 -->
pip --version

<!--更新pyenv -->
cd ~/.pyenv 或者 cd $(pyenv root)
git pull

<!--卸载 pyenv-->
1. 要删除环境变量
2. rm -rf ~/.pyenv


<!--继续配置环境..., 在另一个窗口中将当前docker 保存成镜像, 方便以后使用 -->

docker commit 8bacc8c47607(docker ps.pid) centos_pyenv_368


jsonify 支持对象类型

使flask.jsonify 支持对象类型的dump

flask.jsonify 默认支持字典类型的参数, 返回json化的字符串, mimetype 为 application/json, 要注意的是它不支持list入我们自定义的对象 , 下面尝试扩展其功能

flask.wrapers.Response.data

with app.test_request_context('/', method='POST') as new_context:
    print jsonify(a=[1,2,3]).data

转载sqlalchemy 简单使用

转载自jianshu https://www.jianshu.com/p/8d085e2f2657


这是继SQLAlchemy ORM教程之一:Create后的第二篇教程。在上一篇中我们主要是解决了如何配置ORM系统,建立从类到表的映射的过程,以及如何插入和修改记录。在这个教程中我们主要解决使用的问题。

Query

Sessionquery函数会返回一个Query对象。query函数可以接受多种参数类型。可以是类,或者是类的instrumented descriptor。下面的这个例子取出了所有的User记录。

1
2
3
4
5
6
>>> for instance in session.query(User).order_by(User.id):
... print(instance.name, instance.fullname)
ed Ed Jones
wendy Wendy Williams
mary Mary Contrary
fred Fred Flinstone

Query也接受ORM-instrumented descriptors作为参数。当多个参数传入时,返回结果为以同样顺序排列的tuples

1
2
3
4
5
6
>>> for name, fullname in session.query(User.name, User.fullname):
... print(name, fullname)
ed Ed Jones
wendy Wendy Williams
mary Mary Contrary
fred Fred Flinstone

Query返回的tuples由KeyedTuple这个类提供,其成员除了用下标访问意外,还可以视为实例变量来获取。对应的变量的名称与被查询的类变量名称一样,如下例:

1
2
3
4
5
6
>>> for row in session.query(User, User.name).all():
... print(row.User, row.name)
<User(name='ed', fullname='Ed Jones', password='f8s7ccs')> ed
<User(name='wendy', fullname='Wendy Williams', password='foobar')> wendy
<User(name='mary', fullname='Mary Contrary', password='xxg527')> mary
<User(name='fred', fullname='Fred Flinstone', password='blah')> fred

你可以通过label()来制定descriptor对应实例变量的名称

1
2
3
4
5
6
>>> for row in session.query(User.name.label('name_label')).all():
... print(row.name_label)
ed
wendy
mary
fred

而对于类参数而言,要实现同样的定制需要使用aliased

1
2
3
4
5
6
7
8
9
>>> from sqlalchemy.orm import aliased
>>> user_alias = aliased(User, name='user_alias')

SQL>>> for row in session.query(user_alias, user_alias.name).all():
... print(row.user_alias)
<User(name='ed', fullname='Ed Jones', password='f8s7ccs')>
<User(name='wendy', fullname='Wendy Williams', password='foobar')>
<User(name='mary', fullname='Mary Contrary', password='xxg527')>
<User(name='fred', fullname='Fred Flinstone', password='blah')>

基本的查询操作除了上面这些之外,还包括OFFSET和LIMIT,这个可以通过Python的array slice来完成。

1
2
3
4
>>> for u in session.query(User).order_by(User.id)[1:3]:
... print(u)
<User(name='wendy', fullname='Wendy Williams', password='foobar')>
<User(name='mary', fullname='Mary Contrary', password='xxg527')>

上述过程实际上只涉及了整体取出的操作,而没有进行筛选,筛选常用的函数是filter_byfilter。其中后者比起前者要更灵活一些,你可以在后者的参数中使用python的运算符。

1
2
3
4
5
6
7
8
>>> for name, in session.query(User.name).\
... filter_by(fullname='Ed Jones'):
... print(name)
ed
>>> for name, in session.query(User.name).\
... filter(User.fullname=='Ed Jones'):
... print(name)
ed

注意Query对象是generative的,这意味你可以把他们串接起来调用,如下:

1
2
3
4
5
>>> for user in session.query(User).\
... filter(User.name=='ed').\
... filter(User.fullname=='Ed Jones'):
... print(user)
<User(name='ed', fullname='Ed Jones', password='f8s7ccs')>

串接的filter之间是的关系。

常用的filter操作符

下面的这些操作符可以应用在filter函数中

  • equals:
1
query.filter(User.name == 'ed')
  • not equals:
1
query.filter(User.name != 'ed')
  • LIKE:
1
query.filter(User.name.like('%ed%'))
  • IN:
1
2
3
4
5
6
query.filter(User.name.in_(['ed', 'wendy', 'jack']))

# works with query objects too:
query.filter(User.name.in_(
session.query(User.name).filter(User.name.like('%ed%'))
))
  • NOT IN:
1
query.filter(~User.name.in_(['ed', 'wendy', 'jack']))
  • IS NULL:
1
2
3
4
query.filter(User.name == None)

# alternatively, if pep8/linters are a concern
query.filter(User.name.is_(None))
  • IS NOT NULL:
1
2
3
4
query.filter(User.name != None)

# alternatively, if pep8/linters are a concern
query.filter(User.name.isnot(None))
  • AND:
1
2
3
4
5
6
7
8
9
# use and_()
from sqlalchemy import and_
query.filter(and_(User.name == 'ed', User.fullname == 'Ed Jones'))

# or send multiple expressions to .filter()
query.filter(User.name == 'ed', User.fullname == 'Ed Jones')

# or chain multiple filter()/filter_by() calls
query.filter(User.name == 'ed').filter(User.fullname == 'Ed Jones')
  • OR:
1
2
from sqlalchemy import or_
query.filter(or_(User.name == 'ed', User.name == 'wendy'))
  • MATCH:
1
query.filter(User.name.match('wendy'))

返回列表(List)和单项(Scalar)

很多Query的方法执行了SQL命令并返回了取出的数据库结果。

  • all()返回一个列表:
1
2
3
4
>>> query = session.query(User).filter(User.name.like('%ed')).order_by(User.id)
SQL>>> query.all()
[<User(name='ed', fullname='Ed Jones', password='f8s7ccs')>,
<User(name='fred', fullname='Fred Flinstone', password='blah')>]
  • first()返回至多一个结果,而且以单项形式,而不是只有一个元素的tuple形式返回这个结果.
1
2
>>> query.first()
<User(name='ed', fullname='Ed Jones', password='f8s7ccs')>
  • one()返回且仅返回一个查询结果。当结果的数量不足一个或者多于一个时会报错。
1
2
3
4
>>> user = query.one()
Traceback (most recent call last):
...
MultipleResultsFound: Multiple rows were found for one()

没有查找到结果时:

1
2
3
4
>>> user = query.filter(User.id == 99).one()
Traceback (most recent call last):
...
NoResultFound: No row was found for one()
  • one_or_none():从名称可以看出,当结果数量为0时返回None, 多于1个时报错

  • scalar()one()类似,但是返回单项而不是tuple

嵌入使用SQL

你可以在Query中通过text()使用SQL语句。例如:

1
2
3
4
5
6
7
8
9
>>> from sqlalchemy import text
>>> for user in session.query(User).\
... filter(text("id<224")).\
... order_by(text("id")).all():
... print(user.name)
ed
wendy
mary
fred

除了上面这种直接将参数写进字符串的方式外,你还可以通过params()方法来传递参数

1
2
3
>>> session.query(User).filter(text("id<:value and name=:name")).\
... params(value=224, name='fred').order_by(User.id).one()
<User(name='fred', fullname='Fred Flinstone', password='blah')>

并且,你可以直接使用完整的SQL语句,但是要注意将表名和列明写正确。

1
2
3
4
>>> session.query(User).from_statement(
... text("SELECT * FROM users where name=:name")).\
... params(name='ed').all()
[<User(name='ed', fullname='Ed Jones', password='f8s7ccs')>]

计数

Query定义了一个很方便的计数函数count()

1
2
3
4
5
6
7
8
9
10
>>> session.query(User).filter(User.name.like('%ed')).count()
SELECT count(*) AS count_1
FROM (SELECT users.id AS users_id,
users.name AS users_name,
users.fullname AS users_fullname,
users.password AS users_password
FROM users
WHERE users.name LIKE ?) AS anon_1
('%ed',)
2

注意上面我们同时列出了实际的SQL指令。在SQLAlchemy中,我们总是将被计数的查询打包成一个子查询,然后对这个子查询进行计数。即便是最简单的SELECT count(*) FROM table,也会如此处理。为了更精细的控制计数过程,我们可以采用func.count()这个函数。

1
2
3
4
5
6
>>> from sqlalchemy import func
SQL>>> session.query(func.count(User.name).label('ucnt'), User.name).group_by(User.name).all()
SELECT count(users.name) AS count_1, users.name AS users_name
FROM users GROUP BY users.name
()
[(1, u'ed'), (1, u'fred'), (1, u'mary'), (1, u'wendy')]

为了实现最简单的SELECT count(*) FROM table,我们可以如下调用

1
2
3
4
5
>>> session.query(func.count('*').label('ucnt')).select_from(User).scalar()
SELECT count(?) AS count_1
FROM users
('*',)
4

如果我们对User的主键进行计数,那么select_from也可以省略。

1
2
3
4
5
>>> session.query(func.count(User.id)).scalar()
SELECT count(users.id) AS count_1
FROM users
()
4

在下一篇教程里面我们将会介绍SQLAlchemy对于『关系』的处理方式,以及针对关系的更加复杂的查询。

教程的第三部分传送门SQLAlchemy ORM教程之三:Relationship

使用itsdangerous生成临时身份令牌

使用itsdangerous生成临时身份令牌

在需要身份验证的场景中,可以利用token , 主要流程是由email 向用户下发token, 用户收到邮件证明是本人, 用户点击带有token的链接地址,将token上传后后台 , 后台验证token, 从token中解析出用户信息, 完成一个完整的用户身份验证

1
2
3
4
5
6
7
8
9
10
11
12
13
import itsdangerous
salt='111'
t=itsdangerous.TimedJSONWebSignatureSerializer(salt, expires_in=20)
res= t.dumps({'sid':'flight'})
>>> 'eyJhbGciOiJIUzI1NiIsImV4cCI6MTU2NjE5NDQwOSwiaWF0IjoxNTY2MTk0Mzg5fQ.eyJzaWQiOiJmbGlnaHQifQ.z-56NiU93Jvuus4dezdBcCmveVEBFaqCyHShJPjvgxs'

t.loads(tes)
>>> {u'sid': u'flight'}


<!--过期或改变令牌的任意字符 将失败-->
SignatureExpired: Signature expired
BadSignature: Signature 'GCkAmN6DEOW_oUteJStX8N93W99z_RqMcAOvINpsQd8a' does not match