公司有同事的邮箱账号登陆异常,黑客在搞我们,于是乎,请了乌云白帽子,给我们来了一次安全性测试, 整个过程持续了十来天,第一天开始搞的时候,异常兴奋,从来没有经历过这样的事情,想了解一下黑客 们是怎么做入侵的。提前说一句后话,如果没有乌云的后台,我们还能知道自己被攻击了么?可能要过很久 出现了一些事故后,才能知道,这一点蛮惨的。

以下是乌云后台相关漏洞的截图:

都比较常见,主要是五点: - 开发过程中,没有对用户产生内容做过滤,导致出现xss - 管理不完善,存在大量的后台弱口令 - 开发测试环境没有做隔离 - 员工安全性意识差 - 没有对第三方工具做安全评估

截图中,redis访问,漏洞-数据库,Django开启了debug泄漏敏感信息,这几项属于开发测试环境没有隔离, 外网可以直接访问开发测试环境,一旦出现错误页面,我们的开发框架就会把敏感信息泄漏出去,比如,数据 库连接信息,万一开发测试的数据库的用户密码跟生产环境一致,那就只能呵呵了。redis访问问题,是我们 部分同事,再使用配置redis的时候,绑定的IP是0.0.0.0,结果导致了端口暴露到了外网,外网可以直接 连接上redis,读取相关数据。

运营后台弱口令,某后台限制不严格,大量账号弱口令这些属于管理上的问题,运营同事在设置密码时, 没有使用复杂的密码,而是一些12345619880120,同时,我们运营后台没有对密码试错做处理,给 暴力破解留下了空间。密码设置上,不要出现跟自己身份信息有关的内容,比如,自己的电话号码,生日,姓名 拼音,纯数字或者纯密码,一旦被顶上,用一些简单的社工方法就可以破解。所以,在设置密码的时候,推荐 使用密码生成器,或者密码中加入数字,字母,符号,并且长度不能少于6位。

xss,ios APP存储性xss,这些漏洞属于没有对用户产生的内容做过滤,导致用户可以在发帖子,评论时, 注入js代码,当加载页面的时候,直接执行了注入的js代码,导致用户信息泄漏。防御xss的常见方式是, 对所有用户的输入做转义,有很多现成的库提供了这方面的功能,但是,我们自己在实际开发过程中,缺忽视了 这一点,要命。另外,在设置cookie的时候,需要设置httponly属性,加载js的时候,可以避免 cookie被带过去。对于XSS的常见形式和更多的防御方式,可以参见白帽子讲安全

我们用的项目管理工具,在这次测试中,不幸中招,导致员工信息泄漏,给社工工作做了铺垫,在使用第三方 工具的时候,我们需要对其安全性做评估,防止一些后门,留下安全隐患。

事后,我们对开发测试环境,运营后台访问都做了限制,只有内网才可以访问,并且设置了vpn,让不在公司的 同事,通过vpn访问这些环境,同时,通知了所有同事,修改自己的密码,杜绝后台弱口令。邮箱管理方面,我们 用的是QQ的企业邮箱,让所有同事,都绑定了微信登陆提示,一旦出现登陆失败,或者检测到攻击时,就会给相关 人发微信提醒。在加载页面内容时,统一做了转义,防止注入的js代码在页面上执行。在vpn使用方面,我们强化 了密码管理,登陆vpn的密码是个人密码加谷歌验证的6位校验码。

之前只用过linesman用来给flask做profile,现在的项目在用django, 另外就是,最近的一 次线上日志发现某个请求的响应时间特别长,平均下来得12-13秒的样子,不管是访问量大小,这样的 响应时间是不可接受的。创业公司,难免任务多,时间短,对于性能方面的测试没有多关注,虽然有段 时间集中做了一些优化,从sql到加页面缓存,搜索迁移到elasticsearch等,但是,之后的一些 迭代中,就没有再特别关注。

在上班路上看了几篇关于python性能优化和profile的文章,有一些启发,于是乎,一不做二不休, 在项目中加如了profile功能。开始想用python自带cprofile来完成,但是对于需要对某个特定 函数中,每一行做profile就有些力不从心了。google的时候发现了一个gist,直接拿来用来一下:

class ProfileMiddleware(object):
    def __init__(self):
        if not settings.DEBUG:
            raise MiddlewareNotUsed()
        self.profiler = None

    def process_view(self, request, callback, callback_args, callback_kwargs):
        if settings.DEBUG and ('profile' in request.GET
                            or 'profilebin' in request.GET):
            self.profiler = cProfile.Profile()
            args = (request,) + callback_args
            return self.profiler.runcall(callback, *args, **callback_kwargs)

    def process_response(self, request, response):
        if settings.DEBUG:
            if 'profile' in request.GET:
                self.profiler.create_stats()
                out = StringIO()
                stats = pstats.Stats(self.profiler, stream=out)
                stats.sort_stats('time').print_stats(.2)
                response.content = out.getvalue()
                response['Content-type'] = 'text/plain'
            if 'profilebin' in request.GET:
                self.profiler.create_stats()
                response.content = marshal.dumps(self.profiler.stats)
                filename = request.path.strip('/').replace('/','_') + '.pstat'
                response['Content-Disposition'] = \
                    'attachment; filename=%s' % (filename,)
                response['Content-type'] = 'application/octet-stream'
        return response

简单来讲就是打印出cprofile的结果。

看了输出后,感觉结果完全没用,太多第三方相关的代码列在了结果中,而真正的业务代码却没有被profile 到。继续google后,发现了line-profiler,这个库可以对需要做profile的代码的每一行做检查。 下面是主要的代码实现:

def dispatch(self, request, *args, **kwargs):
    self._rpc_client = getattr(request, 'rpc', settings.RPC_INVOKER)
    self._request = request

    if settings.DEBUG and '_profile' in request.GET:
        profiler = LineProfiler()
        handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
        profiler.add_function(handler)

        profiler.enable()
        response = super(Baseview, self).dispatch(request, *args, **kwargs)
        profiler.disable()

        out = StringIO()
        profiler.print_stats(stream=out)
        response.content = out.getvalue()
        response['Content-type'] = 'text/plain'

    else:
        response = super(Baseview, self).dispatch(request, *args, **kwargs)

    return response

输出结果就是我想要的,不错:

Timer unit: 1e-06 s

Total time: 8.63884 s
File: index.py
Function: get at line 291

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
   291                                               def get(self, request, *args, **kwargs):
   292         1           10     10.0      0.0          request.logger.app(nothing=True)
   293                                           
   294         1           44     44.0      0.0          start_num = int(request.GET.get("start_num", "0"))
   295         1           13     13.0      0.0          tabtype = int(request.GET.get("tabtype","0"))
   296         1            2      2.0      0.0          count = 10
   297         1            2      2.0      0.0          error = 0  
   298         1            2      2.0      0.0          data = {}
   299         1          135    135.0      0.0          pos_0 = slide_adapter_future(position=0)
   300         1      8355318 8355318.0     96.7          data['slides'] = pos_0.unwrap()
   301         1           88     88.0      0.0          map(lambda x: _prune_slide_keys(x), chain(data['slides'],))

references

  1. line_profiler source code
  2. django cprofile middleware
  3. 检测Python程序执行效率及内存和CPU使用的7种方法
  4. 使用cProfile分析Python程序性能
  5. The Python Profilers

随便翻阅了一下python 高手之路,真好最近也真在做一些持续继承的准备工作,看到tox 的介绍感觉很棒,亲自试了一下,还是挺好用的,但是,过程中遇到几个坑,记录一下吧!

首先,我们的项目没有setup.py文件,但是tox默认是去找这个文件的,所以需要在设置 中增加以下这行:

[tox]
skipsdist = True

其次,也不是tox的问题,是pipjpushv3.0.1闹腾的,pip在安装的时候,不会根据 requirements.txt中的次序逐个安装,而是自己去找相关的依赖,恰好这个jpush版本有 问题,jpush v3.0.1依赖了requests,写在依赖文件中,但是安装的时候就会报错,无法 找到requests,彻底奔溃,解决办法:

[testenv]
deps =
    requests==2.5.3

commands=
    pip install -r {toxinidir}/requirements.txt --index http://pypi.douban.com/simple
    python manage.py test tests

现在环境准备的时候安装requests,之后在测试的命令中,安装依赖。

最后,也是最坑爹的,自己的依赖有一些是公司内部的repo,而用tox创建新的环境后,一些 环境变量并没有带入到测试的虚拟环境中,导致,找不到ssh-key,然后安装就悲剧,解决 办法:

[testenv]
passenv = SSH_AUTH_SOCK

最后,完整的tox.ini文件内容如下:

[tox]
envlist = py27

indexserver =
    default = http://pypi.douban.com/simple

skipsdist = True

[testenv]
envdir = {toxinidir}/.env

passenv = SSH_AUTH_SOCK

deps =
    requests==2.5.3

commands=
    pip install -r {toxinidir}/requirements.txt --index http://pypi.douban.com/simple
    python manage.py test tests 

references

  1. using tox django projects
  2. tox, pip, git, and ssh-agent
  3. Tox tricks and patterns

开发基本告一段落,开始对代码进行一些优化,使用的flask框架,做profile的时候,使用 linesman来记录profile的数据,并展示。linesman比较给力的一个地方是,它的web界面很 好用,可以直观地看到应用在哪里话费的时间比较多,方便定位问题所在。

下图是没有优化前,一个HTTP请求的调用时间,已经call stack:

可以看到用黑笔标记的部分,每次发起HTTP请求,都会去解析外部服务的地址,导致请求的 时间变长,影响整体的响应速度。首先,我们把两次HTTP请求合并成一次,这样在一定程度上 加速了应用,减少的时间就是一次HTTP请求的时间,大概在0.2-0.3s之间。

但是,整体的响应速度还是不理想,在0.7-0.9s之间,之后,我们把DNS做了cache,这样, 在调用外部服务时,DNS解析部分就节省了大量的时间,优化后的profile如下:

DNS cache原理如下:

import _socket
import socket

import requests


prv_getaddrinfo = socket.getaddrinfo
dns_cache = {}
def new_getaddrinfo(*args):
    try:
        print 'got cached'
        return dns_cache[args]
    except KeyError:
        res = prv_getaddrinfo(*args)
        dns_cache[args] = res
        print 'here'
        return res
socket.getaddrinfo = new_getaddrinfo

一直在找python web应用profile的工具 我们使用了flask 昨天使用werkzeug的profile中间件 但是生成的结果日志没办法图像化 看起来很很费力 最终选择放弃

Google许久 总算找到一个比较不错的profile工具 可以直接通过web界面看到profile的结果 并且可以得到图像化调用栈 该库的地址 http://pythonhosted.org/linesman/narr/getting_started.html

使用方法:

from linesman.middleware import make_linesman_middleware

app.wsgi_app = make_linesman_middleware( app.wsgi_app, profiler_path="/profiler") 上述代码中的profiler_path是可以通过web访问的uri 完整url为:http://yourtestserver:port/profiler

结果截图: