• OT环境下IoT安全的破局探索

    下面的PPT是之前在一次沙龙上分享的,偶然翻到,分享一下

    产品链接:https://www.aliyun.com/product/developerservices/cmn (不是广告,而是和云网管合作的一个IoT安全网关产品,作为这个产品的PM,不正经的体验了一次产品创造的快乐)

    配合这两篇文章食用味道更佳

    2020 IoT Threat Report

    网络安全架构体系参考——操作技术(OT)安全架构参考

    在2019年就开始尝试去解决OT环境(物流仓)下的安全风险了,尝试了很多方向,但是这个领域可以借鉴的实在太少,OT环境下的网络环境是非常脆弱的,一个网络设备故障、一个小小的勒索病毒往往就会使整个仓库停摆,边分析风险CASE,边优化产品方案,直到看到《2020 IoT Threat Report》这篇文章,发现设计的方案和文中的观点基本一致,间接验证了方向是对的,那剩下的就是去落地了。

    无论是线上还是线下,资产管理永远是个难题,如果管控OT环境中的资产,知道有多少、有哪些、新增哪些、减少哪些?这是第一个要解决的问题,要解决它,那就必须要有IoT资产识别的能力,维护资产指纹是个耗时耗力的工程。但是一旦解决了这部分,就具备了IoT设备准入、风险设备阻断的能力,所以这是必须要去做的部分。

    影响OT环境网络稳定性的风险有两类,网络风险和安全风险。

    1. 网络攻击导致仓内作业链路中断

      恶意代码、勒索病毒、拒绝服务等

    2. 网络故障导致仓内作业链路中断

      线路中断、DNS故障、网络设备故障等

    要解决网络风险,就要具备OT环境的风险识别能力,但是OT环境中设备的多样性,不可能在所有端上部署AGENT,同时网络环境的脆弱性,也不允许将网关串联到网络中,所以自然而然,旁路流量镜像+云上流计算风险识别就成了最优解。

    剩下还有一个网络稳定性的风险,这个过于专业,需要专业的产品来做,CMN是阿里集团内部自动化运维产品的商业化,能力经过检验,最适合来解决这个风险,同时安全能力也能提高CMN的产品竞争力,一拍即合。

    更具体的架构看下文的ppt,许多技术细节因为是内部产品不便公开,另外这个项目是笔者在应用安全工作之外尝试做的部分,过程中有无数次想摆烂,不过回想起来还是蛮有趣的一段经历,有感兴趣的可以在数据安全的前提下一起探讨。

    对了,它的名字叫“磐石盾”。



















  • 我的应用安全方法论:路在脚下

    在2020即将结束之际,将自己对于应用安全建设的一些思考记录成此文,与诸位探讨未来的应用安全之路,一家之言,不要当真

    在前几年,各种文章/会议,每当提起应用安全,PPT总会出现类似的这张图

    这几年又都换上了这张图

    仿佛用原来的那些安全能力,照着DevSecOps的流程重新画一个图,就可以将原先解决不了的难题解决掉,但真的是这么简单吗?

    一、应用安全困境

    经常看到某些安全公司各种画大饼,说网络安全从业缺口xxx万,听起来这个行业仿佛是个蓝海,但实际上,哪个公司会去雇用这么多的安全工程师呢?在一二线互联网公司,“应用安全工程师:开发工程师=1:500”应该是个很常见的比例,甚至有些公司会更高,攻防只会是工作中的极少部分,更多的则是“柴米油盐”。

    在应用的开发速度上,也和以前有了很大的变化,商业环境的瞬息万变,促使其要不断的、快速的开发新应用来支撑新的业务,例如社区团购、全民买菜等,需要在很短的时间内上线,并且要不断的进行迭代,应用的运行环境也发生了很大的变化,Web、APP、小程序、云原生等等。

    简单总结一下现在遇到的困境:应用数量、场景的快速增加、功能迭代周期短,原来的SDL流程无法融入到新的开发模式中,致使很多风险发布到线上才被发现或直到被利用还没有感知。

    在笔者这几年参与应用安全建设的过程中,对其有着很深刻的感触,很多时候,面对那么多的应用,那么多的风险,深深的无力感。

    出现问题的时候,其他人总是会问或者扪心自问,这个风险出现的原因是什么?为什么没有提前发现?后续可以覆盖到吗?现在或许可以试着回答一下这个问题。

    二、我认为的应用安全发展趋势

    经过这几年的实践总结,在这里和大家探讨下如何“困境突围”。

    首先,对于DevSecOps,其中的很多观点笔者是比较认同的,但是和很多人的认知不一样的地方是,更多的是将其视作一个方法论,就像SOAR、UEBA等一样,而不是其一个执行框架。在SDL的阶段,很多人在衡量应用安全建设完成度的时候,会将每个环节是否都覆盖作为其标准,但这显然是一个伪命题,100%的覆盖度在现实中是不存在的。

    在笔者看来,应用安全的发展趋势有这么三点:

    1、业务、产品、开发、测试、安全五者之间合作关系的转变,所有人都要为应用安全负责;

    2、安全风险的发现要融入到“开发-测试-部署”的流程中去,在上线前发现风险;

    3、安全自动化(安全运营可持续化、安全流程自动化、风险感知自动化)。

    单从这几个观点来看,似乎和DevSecOps中的某些观点有相似之处,事实也确实如此,但是需要再强调一遍,DevSecOps更多的是一种方法、一种思想,我们可以学习其长处,切不可照猫画虎。

    三、我对现在SDL中一些流程的看法

    在讲笔者是如何讲上述三个观点落地之前,先谈一下笔者对其现在SDL流程中一些大家都在做的流程的看法:

    1、应用安全/安全开发/安全意识培训时讲漏洞类型(OWASP Top10等)是一种低效的行为;

    根据笔者培训的经验来看,真实的case是直击人心且令人印象深刻的,用一个小时讲实际中漏洞类型,特别是XSS、CSRF等,会使听众昏昏欲睡。这里不是说安全培训不重要,恰恰相反,安全培训是提升开发安全意识的一个重要手段,关键是面对不同的人群针对性的去选择培训素材和培训方式,如果能达到“开发主动来咨询安全解决方案”的效果,那培训便算得上是成功的。

    2、强制性的流程卡点是一种偷懒的行为;

    做安全运营的同学往往有种思维惯性,希望在所有环节都做上卡点,这种行为/想法是非常不利于合作关系转变的。换位思考下,谁希望每一步操作都要受到阻碍,与开发效率提升相背而行。

    四、我的应用安全方法论

    回到正题,对于应用安全的建设,笔者是如何探索落地的呢?下边一一道来。

    首先,我们要清楚,应用漏洞的种类那么多,在大公司,要保护大量应用、海量数据的安全,最应该解决的是哪几种风险呢?

    1、数据批量泄漏的风险;

    2、权限管控不完善的风险;

    3、命令执行/SQL注入/服务器接管等高危漏洞的风险。

    或许这里会有人提到监管风险,例如网络安全法、GDPR、等保等,因其往往属于安全合规或数据安全的范畴,且不在笔者的认知范围内,这里暂且不提。

    理清了关键风险,那么怎么做就会更清晰一点了,笔者讲其方法论归纳为三个环节来谈,分别是:

    1、应用安全开发:在漏洞代码上线前发现风险;

    2、应用安全评估:通过红蓝对抗、安全众测、漏洞扫描等发现已上线的漏洞;

    3、应用异动感知:高危/核心应用的异动监控、感知。

    第一点:应用安全开发

    我们的目标是尽可能的避免存在漏洞(特别是高危漏洞)的代码发布到线上。

    这里笔者将漏洞分为常规漏洞(SQLI、XSS、SSRF等)、业务逻辑漏洞(越权漏洞、短信爆破等)两类。

    常规漏洞:这类风险就依赖安全开发框架、DAST、IAST、RASP等等安全工具了,工具链越完善,该类漏洞就会越少,这本就是安全专业范围内的事情,本文就不展开讲了。

    业务逻辑漏洞:现在不管哪家的SRC,收到的高危漏洞中,越权漏洞一定是占比最大的部分,至今也没有看到一个运营成本低、误报率低的越权类漏洞扫描器的出现。其实这个也可以理解,前几年笔者也试图去开发一个越权扫描器,但是效果却不如想象中的美好,究其原因就是,业务形态的多变、开发风格的不同,导致了越权漏洞表象上大多一样,原理逻辑上却大相径庭,这也就是现在基于流量重放的越权扫描器效果相对好一些的原因所在。

    既然越权扫描器解决不了全部的业务逻辑类漏洞,那么试着去改变一下生产关系呢?业务、产品、开发、测试、安全五者之间合作关系的转变,所有人都要为应用安全负责。

    解决业务逻辑漏洞的方法笔者将其拆解为三个动作:架构安全评审、QA覆盖、风险复盘。

    I. 架构安全评审

    为什么要做架构评审呢?原因可以参见这篇文章微服务架构下的越权风险,开发方式、开发架构的转变,出现了很多类Gateway的应用,这些应用的架构如果没有安全参与评估,往往会导致整个系统出现框架性的权限失效,且修复起来难度极大。

    这里要注意的是,不是所有应用都要去参与架构安全评审,不然很容易又陷入到“人力配比”的困境中去,根据实际情况来选择评审策略。

    II. QA覆盖

    QA环节的人力往往是充裕的,并且会将覆盖所有的发布和迭代,那么试想一下,如果测试同学帮我们做一部分安全测试的工作,那岂不是既可以大幅提高覆盖度,又可以将安全的经历集中到其它环节。

    那怎么去做呢?第一,要清楚业务逻辑类漏洞,既是漏洞、又是BUG,达成共识的关键;第二,测试同学不是专业的安全,所以不可能覆盖所有的漏洞类型,要有选择的有的放矢,侧重于越权等风险类型;第三,那就是风险复盘了。

    III. 风险复盘

    即使前边经过了各种评审和测试,一定还会有漏洞被发布到线上,这时候就要进行深度的风险复盘,复盘的目的是什么呢?找出漏洞出现的根本原因,如果是应用架构有问题,那便拉项目,改架构,如果是人的问题,那便要思考下安全意识的问题了。至于哪些风险需要复盘,这个则要根据当前的安全形势,有选择性的去复盘。其中一个关键点就是,开发、产品、测试都要参与到复盘中去。

    第二点:应用安全评估

    红蓝对抗、安全众测、漏洞扫描等详细的流程本文也就不展开说了,有非常多的文章介绍这些方面,笔者着重谈一下该环节的重要性。

    安全评估环节相当于是一个查漏补缺,检验安全开发环节效果的作用,要同黑灰产进行时间竞赛,力争先于他们发现风险。

    该阶段发现的风险,每一个风险都是投入了非常多人力、物力、财力换来的,要充分的利用其价值,而不是修复了事。

    每一个通过该环节发现的高危风险,都应该与“风险复盘”进行闭合,深入其中发现出现风险的根本原因。

    第三点:应用异动感知

    笔者根据应用安全中的风险,将异动感知划分为三个部分,分别是:爬虫攻防、漏洞利用、非漏洞性数据泄漏。

    I. 爬虫攻防

    说起爬虫攻防,笔者真是一把辛酸泪,在面对海量的流量时,如何准确的区分机器流量和正常流量就已经是一件很困难的事情了,如果在机器流量中快速准确的发现爬虫流量,特别是高级爬虫,是非常有挑战的一项工作。每个公司有不同的爬虫对抗平台,或者是选择爬虫风险管理,此处笔者不再赘述。

    II. 漏洞利用

    此处“漏洞”更多指的是“命令执行/SQL注入/服务器接管等高危漏洞”,特别是近几年,供应链漏洞频发,在应用安全中,一旦没有有效的全覆盖的推动供应链漏洞修复,则极有可能被外部利用攻击,那我们该怎么办呢?常见的感知方法有两个,分别是:蜜罐、WAF,当然有些团队会基于各种流量日志构建攻击检测平台,检测各种payload,以达到在漏洞被利用时快速发现的目的,笔者因没有构建过类似系统,更多的是使用,所以细节相关的就不多讲了。

    III. 非漏洞性数据泄漏

    “非漏洞性数据泄漏”怎么理解呢?讲一个场景,例如某个客服的账号被盗用,然后将该客服权限下的数据批量盗取,那这种风险如何避免呢?有些同学可能会认为这不是应用安全的范畴,但实际上,只要是从应用上泄漏的数据,都是属于应用安全该去防护的范围。

    这里笔者推荐的方法是UEBA(获取多个维度的行为数据、日志,将其通过基础分析方法、高级分析方法,进行建模分析,识别异常行为的风险用户和风险实体,然后通过打分评级的方式输出告警,使安全工程师可以优先级处理告警),针对不同的高风险敞口进行监控。目前市面上也有不少基于UEBA方法论的产品落地,各家公司内部也或多或少会有类似实现,分析算法都已经很成熟了,更多的还是根据不同的业务场景制定不同的策略。

    本文没有像其它介绍企业安全建设的文章那样将方方面面都讲到,主要集中在应用安全的视角,来探讨如何保证应用的安全。上述提到的很多个点可能看起来有点独立,其实并非如此,因为还有一个方法论没有提到,那就是SOAR,可参看从SOAR中求解应用安全建设强运营突围之法,将各个环节自动化的进行编排,使安全能在整个DevOps流程中游刃有余。如果只是将SOAR看作是一个有控制台拖拖拽拽的样子,那就有些狭隘了。

    有许许多多的方法论,许许多多的安全建设的文章,但“听过很多道理 却依然过不好这一生”,每个公司的实际情况都不一样,如一味的追求方法论的完成落地,往往很难取得很好的效果。应用安全建设之路在何方?路在脚下。

    [1]文中部分图片来源自网络
    [2]文中部分参考文章链接来源公众号:电驭叛客

  • 微服务架构下的越权风险

    一篇小文,主要是通过安全的视角来简单讨论一下微服务架构下容易出现的一些去权限安全问题,涉及到开发细节说错的时候还望谅解。

    一、微服务与微服务架构

    微服务是一种开发软件的架构和组织方法,其中软件由通过明确定义的 API 进行通信的小型独立服务组成。这些服务由各个小型独立团队负责;采用UNIX设计的哲学,每种服务只做一件事,是一种松耦合的能够被独立开发和部署的无状态化服务。

    微服务具有两个关键特性:自主性、专用性,具有敏捷性、灵活扩展、轻松部署、技术自由、可重复使用的代码、弹性等优势[1]。

    从上边的描述可以看出,微服务更多的是一种方法,而微服务架构则是实践微服务的具体实现,不同厂商、不同开发的实现方式各有差异,大概架构基本如下图[2]。它使得开发可以专注自由的开发他负责的服务,解决了系统的复杂性,可以独立部署,与DevOps开发流程契合等,这也难怪现在几乎全民微服务了。

    但是哪有什么银弹,优点越多的技术带来的负面影响往往越大。

    二、微服务的访问权限困境

    在微服务架构下,一个应用被拆分为多个微服务,这样一来,原先单体架构下的权限体系就不再能满足微服务的鉴权需求了,每个微服务都需要对自身API进行权限管控,需要对接账号体系和明确哪些角色有权限访问;同时还有来自服务之间的调用等。

    在这种情况下,无论是通过session方案还是token方案,都需要开发去自己维护这一权限体系,那么问题就来了。业务产品在后边追,各种发布封网在前边拦,往往会为了完成业务功能而取舍掉权限的部分,又或者不同的开发同学对于鉴权的理解不一致,甚至认为判断有没有登陆就认为已经完成了权限校验。

    随着这类API的越来越多,就会发现同类安全问题频繁发生,特别是未授权访问这类低级的漏洞。

    三、绕不过的API Gateway

    一方面为了管理乱糟糟的API调用关系,一方面为了解决上边提到的风险,微服务的调用需要一个统一管控的部分,也就是API Gateway。现在是不是就不会出现越权漏洞了呢?

    想法是美好的,但现实往往啪啪打脸,Gateway的架构缺陷往往会造成更大的风险。

    • API Gateway 仅仅是个出入口

    这类网关往往只起到了一个路由的作用,它可能只有一个域名和简单的路由转发功能,将同一部分业务的API通过同一个域名开发出去,将API的权限校验全部交给相应的开发去搞,当出现未授权访问漏洞时,双方就开始扯皮,这个权限校验不是你应该做的吗?特别是当这业务特别庞大时,几百几千个API都接入,这可真是个灾难~

    • API Gateway 通过黑名单控制访问权限

    有时候需要控制一部分API开放公网另一部分不对外开放,有时候一部分API需要访问控制另一部分可以公开访问等。在解决这类需求时,很多开发会把API Gateway设计成黑名单模式,例如不能对外开发的API需要添加到blacklist中,又或者需要做菜单访问控制的API需要主动去配置。从文字上,这貌似没有什么问题,合情又合理,但是回到人性上,人是健忘的和怕麻烦的,所以就导致经常出现内部API忘了配置blacklist开放公网或忘了配置权限菜单造成未授权访问漏洞的情况。

    • API自动编排服务

    这两年出现了一些提供快速搭建应用能力的平台,通过网关能力,编排能力以及多种类型的服务对接、统一开放的服务市场,来完成服务发现,服务生产,服务消费的全链路的服务流程。这对于非开发的同学开发应用或帮助开发同学快速发布应用,是很有意义的,但是随之而来的就是它的权限校验问题,在编排时,权限判断逻辑是否是必选项、接入账号体系后是否有角色配置关联、是否支持数据权限的校验等等,任何一个点出现问题,整个平台的应用可能都会存在风险。

    随着这类情况越来越多,通过应用数量来看业务规模已经不太行的通了,有时候一个应用可能会有几百上千个API[3],背后又对应着几十上百个开发,对于传统的SDL来说太难落地了[4],关于API的安全治理,是一个复杂的体系,有机会细聊。

    (这难道不是越权漏洞挖掘的思路嘛)

    四、基于API Gateway权限校验的一些想法

    实际的API Gateway设计涉及到多个方面,安全和性能往往在一个平衡的状态,下边的仅供参考

    • 安全工程师可以不介入到每个应用进行安全评审,但是API Gateway类的应用一定要覆盖和持续跟进

    • 在调用者和被调用者中间加一层网关,每次调用时进行权限校验。充分利用API网关,完成将身份验证和路由功能

    • 在Gateway或账号管理平台上维护API与角色的关系,在Gateway判断API与角色是否匹配,可以解决访问权限(垂直权限)越权的风险

    • 当通过API Gateway控制router时,公网可访问的列表用白名单方式,只有主动添加才可以对外开放

    • 每个微服务的开发都应该为其数据访问权限负责

    五、这个世界是一个圆

    关于Service Mesh架构我们下次再聊。

    每一次开发架构的更迭,都会带来新的安全挑战,现在越来越火的Serverless、FaaS等也一定会带来一系列新的安全挑战,说不定再过一段时间又回到了最初的单体架构。

    参考:

    [1]https://aws.amazon.com/cn/microservices/

    [2]http://dockone.io/article/3687

    [3]API安全全链路解决方案写了好久,但是还有几个关键点在实践场景中没有跑顺畅,再等等(没有实践,没有发言权)

    [4]DevSecOps同样,在实践的路上,还不敢拿出来班门弄斧

    [5]图片来源网络

  • API越权风险检测方式浅谈

    timg

    0x01 前言

    越权漏洞相较于SQLInject、XSS、SSRF等漏洞,最大的不同点在于该漏洞和权限的架构设计具有强相关性,而权限的架构设计又强依赖业务属性,这就导致了几乎每个系统的权限架构都各不相同,自动化检测的难度非常之大,误报率非常高;内部的检测准确率无法有效提升,越权漏洞数量就很难有效的下降

    关于越权漏洞的挖掘,从原理上来看并不复杂,甚至可以说是简单,最大的难点在于信息收集能力、细心程度、对业务的理解度,这个其实也没有什么好谈的

    关于批量/自动化越权检测的文章在网路上寥寥无几,开源的工具更是少的可怜(某些大佬手里有牛逼工具除外),今天想要说的是在甲方,特别是应用数量三位数、四位数以上的甲方,在面对大量API时的越权漏洞检测思路探讨

    0x02 越权风险类型

    越权漏洞的分类及定义在[1]这个笔记里说的已经很详细了,在这里就不赘述了

    • 未授权访问

    • 水平越权

    • 垂直越权

    除了上边说的三类权限漏洞,结合业务场景,一般会将权限分为两类

    • 功能性权限/菜单权限
      • 用户有没有权限访问这个功能/菜单
    • 数据性权限
      • 用户有没有权限访问其它用户的数据

    0x03 检测方式

    一个应用,可能只需要一两个小时就可以测试完成;十个应用,顶多也就一周;那一百个呢?一千个呢?

    每天又会有几百个应用产生迭代,新增的API呢?

    实际上一家几千人的互联网公司,负责应用安全的同学往往只有两到三个,API会有几千几万个

    所以这里我们从黑盒、白盒、自动化、半自动的思路来探讨一下越权漏洞的解法

    I. 黑盒+自动化

    主要分给两个关键部分

    • 流量采集
      • 服务器出口日志采集
      • 测试环境日志采集
    • 请求重放
      • 多个账号重放请求
      • 结果对比
      • 排除误报

    优点:在理想情况下,可以通过很少的人力去覆盖大量应用

    缺点:大量的误报,安全产品死于误报;线上真实流量重放效果最好,却容易造成故障

    II. 黑盒+半自动化

    这种方式一般会尝试和测试团队合作,在测试环境里进行,或者安全工程师进行单个系统测试时使用,像ZTO的authcheck、BurpSuit插件AuthMatrix等差不多都是类似的思路

    • 流量采集
      • 浏览器被动代理或爬虫方式采集
    • 请求重放
      • 多个账号重放请求
      • 对response进行简化
    • 人工确认
      • 对简化后的response进行判断是否存在漏洞

    优点:最后输出的漏洞结果准确度较高,适合针对单一系统进行安全测试

    缺点:在面对大量应用及大量迭代新增API时,效率太低

    III. 白盒+自动化

    这种方式有两种思路,一种是发现存在越权风险的接口,一种是发现不存在越权风险的接口,原理是差不多的

    • 白盒扫描
      • 入参是否包含可越权(可遍历)的参数
      • 是否有从cookie或session中或获取用户标识
    • 风险确认
      • 是否包含authcheck(xxid, userid)的判断逻辑
      • 是否包含@authcheck的注解

    优点:仿佛看到了可以批量发现越权风险的方法,对于一些简单的水平越权可以有效发现,而且一般一个系统的权限缺陷都是相似的,发现一个,发现一片

    缺点:误报率太高,不能发现复杂的越权风险

    IV. 白盒+半自动化

    这种思路和上面提到的”黑盒+半自动化“的思路差不多

    • 白盒扫描
      • 扫描器应用的API list(可参考我上一篇文章[2])
      • 标明入参及用户标识、判断逻辑
    • 人工确认
      • 通过白盒扫描出来的入参、用户标识、判断逻辑来判断是否存在越权风险

    优点:对一部分水平越权、未授权访问的风险可以有效发现

    缺点:API数量一多就不灵光了,很难发现复杂的越权

    0x04 简单思考

    上边扯了这么一堆越权漏洞的检测方法,但其实并没有解决上边提出的问题

    上千个应用、几万个接口,每天新增上百个,如何去解?存量呢?新增呢?

    在以前,我还经常会去给开发们做一些安全培训,但是效果呢?只有少数开发能真正认识到越权的严重性,效果并不乐观,该出现的漏洞一个都没少,并且效果无法衡量

    做应用安全也有几年的时间了,现在所谓的”安全运营”也越来越多人提了,那我们是运营吗?是也不是,我更喜欢称自己为“技术运营”,用技术来解决运营的困境,这个愿望是好的,扯皮的事情总是少不了的

    回到越权漏洞的问题,每次做安全众测,都会爆出来一堆越权的问题,不乏之前出现类似漏洞的应用,然后就会有人跳出来问,你们之前复盘的action落实了吗?为什么还会出现?balabala,这些问题我又何尝不是经常问自己,到底是为什么?这段时间和教父也经常在讨论这些问题,那么问题出在哪里了呢?

    先说一下我的思考结论,越权漏洞的解法是”流程+工具(监控)+覆盖度+蛮力“

    • 流程
      • 举个例子,当SRC上报了一个越权漏洞过来,开发完成修复,然后进行复盘(出现这个问题的根本原因是什么?其它API会存在吗),开发领了排查同类API的action回去,排查完成后,安全工程师核查开发同学的排查结果,形成闭环
    • 工具(监控)
      • 工具这个范围就很广了,在我看来,无论是开源的还是自己开发的工具都像是一块块积木,将它们通过合适的方式组合起来,才能发挥出最大的效果;而不是为了kpi而不停的造轮子,造完一个来年再造一个
      • 针对增量的API,建立完善的监控体系
    • 覆盖度
      • 这个点是最关键的一个点,你会发现,每次出事的点都是你没有覆盖到的,对资产的熟悉程度,对API的监控完整度都非常重要

    但这个也不是我真正想表达的点,因为按照上边的说法,还是要搞一套类SDL的东西出来,API那么多,人就这么两三个,到年底又变成PPT上代码了,第二年问题照旧

    虽说在这类风险的解决上没有一招鲜的讨论,但是我觉得真正需要去做的是逐个点打穿,一步步的去收敛风险;将一个个点打穿,才能真正的解决问题,而不是浮于表面,看起来大而全,实则只不过是徒有其表;没有流程、技术支撑的东西我是不信的,不要再提什么宣导啥的,没什么用处

    例如这个阶段就只做”复盘–开发自查–安全复查–安全挖掘漏洞–复盘”这个闭环,相信用不了半年,就可以对高危应用的风险进行有效收敛

    上边没有提“蛮力”这个点,有时候要解决风险,地毯式排查往往是最有效的方法;为什么这么说了,假设现在开始治理越权,那几千几万个存量API怎么处理呢,等扫描器开发完成?这时候可以对API进行分级,识别出其中包含敏感信息的API,这时候就剩几千个了(也是非常庞大的工作量,但是只能硬着头皮上),对这部分API进行地毯式排查,没有敏感信息的越权,危害还是相对可控的;方式low了点,但效果是有的,同时建立完善的新增接口监控体系,对增量API及时的进行处理;在评审了大量的接口后,归纳总结,对每个业务域的权限架构进行优化(当然这非常困难),安全接入进去,从根本解决问题(这种方案是笔者现在正在实践的,效果半年后再来写一篇)

    现实有时候是骨感的,干活的人没啥增加,每年的kpi都是不断的增长,很难专门拿出一段时间来做存量的攻坚,存量不解决,漏洞永不休

    //说了这么多,下班回家脑阔疼,不想调整逻辑了,大家将就着看一下,想表达的观点就一个“找到问题,打穿它,无论用什么方法,有些苦是一定要吃的”。

    //还有就是教父说招人一起来搞事情

    参考:

    [1]https://g.yuque.com/evilm/yuequan/gmqet8?language=en-us

    [2]https://mp.weixin.qq.com/s/ATpoEN9QI-D5vkxDimQ8FQ

  • JavaParse(AST)获取Java Web API list

    AADB6CC4-5C27-4742-8D9A-79B303C6D550

    分享一个人工审计代码时的小Tips

    在审计一个复杂的Web应用时,可能会有几百个WebAPI,如果没有一个API list,要想不漏掉的审计完还是非常费脑筋的

    0x00 原理

    通过JavaParse解析Java文件,解析出WebAPI list,简单实现效果如下

    关于JavaParse

    Analyse, transform and generate your Java code base.

    In its simplest form, the JavaParser library allows you to interact with Java source code as a Java object representation in a Java environment. More formally we refer to this object representation as an Abstract Syntax Tree (AST).

    0x01 目标

    目标:将下图代码中的WebAPI解析出来

    0x02 效果

    解析完成后的效果大致是这样的,然后在针对要筛选的条件,对api进行筛选即可,例如RequestMapping

    1
    2
    3
    4
    5
    6
    7
    8
    9
    [
    {"name":"public List<██████████> ██████████(@RequestParam String ██████████)","body":"Optional[{██████████},

    {"name":"public String ██████████(@RequestParam String ██████████)","body":"Optional[{██████████},

    {"name":"public List<██████████> ██████████()","body":"Optional[{██████████},

    {"name":"public String help()","body":"Optional[{██████████}
    ]

    0x03 代码实现

    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
    package net.uxss.b1ueb0ne.javaparse;

    import ...

    @Component
    public class MethodDeclare {

    public JSONArray declareParseCode(String code) {
    JSONArray declareJsonArray = new JSONArray();
    CompilationUnit compilationUnit = JavaParser.parse(code);
    //解析Java源代码并创建抽象语法树。
    //解析源代码。它从提供程序获取源代码。开头指示可以在源代码中找到的内容(编译单元,块,导入...)
    try {
    TypeDeclaration declaration = compilationUnit.getType(0);
    //返回在此编译单元中声明的类型的列表。
    List<BodyDeclaration> list = declaration.getMembers();
    //获取这个类里面的成员
    for (BodyDeclaration bodyDeclaration : list) {
    //枚举成员
    Map<String, String> declareMap = new HashMap<>();
    if (bodyDeclaration.isMethodDeclaration()) {
    //判断是否为方法
    MethodDeclaration declareParse = (MethodDeclaration) bodyDeclaration;
    declareMap.put("name", declareParse.getDeclarationAsString());
    //获取方法名
    declareMap.put("body", declareParse.getBody().toString());
    //获取方法body
    }
    JSONObject declareJson = JSONObject.parseObject(JSON.toJSONString(declareMap));
    //解析成字符串
    declareJsonArray.add(declareJson);
    }
    compilationUnit.accept(new VoidVisitorAdapter<Void>() {
    //不返回任何内容的访问者,其所有访问方法都有一个默认实现,该实现只是以未指定的顺序访问其子方法
    @Override
    public void visit(MethodDeclaration n, Void arg) {
    super.visit(n, arg);
    }
    }, null);
    }catch (Exception e){
    System.out.println(e);
    }
    return declareJsonArray;
    }
    }

    0x04 parse方法详解

    com.github.javaparser.JavaParser#parse

    1
    CompilationUnit compilationUnit = JavaParser.parse(code);
    1
    2
    3
    public static CompilationUnit parse(String code) {
    return simplifiedParse(COMPILATION_UNIT, provider(code));
    }

    com.github.javaparser.JavaParser#simplifiedParse

    1
    2
    3
    4
    5
    6
    7
    private static <T extends Node> T simplifiedParse(ParseStart<T> context, Provider provider) {
    ParseResult<T> result = new JavaParser(staticConfiguration).parse(context, provider);
    if (result.isSuccessful()) {
    return result.getResult().get();
    }
    throw new ParseProblemException(result.getProblems());
    }

    com.github.javaparser.JavaParser#parse

    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
    /**
    * Parses source code.
    * It takes the source code from a Provider.
    * The start indicates what can be found in the source code (compilation unit, block, import...)
    *
    * @param start refer to the constants in ParseStart to see what can be parsed.
    * @param provider refer to Providers to see how you can read source. The provider will be closed after parsing.
    * @param <N> the subclass of Node that is the result of parsing in the start.
    * @return the parse result, a collection of encountered problems, and some extra data.
    */
    public <N extends Node> ParseResult<N> parse(ParseStart<N> start, Provider provider) {
    assertNotNull(start);
    assertNotNull(provider);
    final GeneratedJavaParser parser = getParserForProvider(provider);
    try {
    N resultNode = start.parse(parser);
    ParseResult<N> result = new ParseResult<>(resultNode, parser.problems, parser.getTokens(),
    parser.getCommentsCollection());

    configuration.getPostProcessors().forEach(postProcessor ->
    postProcessor.process(result, configuration));

    result.getProblems().sort(PROBLEM_BY_BEGIN_POSITION);

    return result;
    } catch (Exception e) {
    final String message = e.getMessage() == null ? "Unknown error" : e.getMessage();
    parser.problems.add(new Problem(message, null, e));
    return new ParseResult<>(null, parser.problems, parser.getTokens(), parser.getCommentsCollection());
    } finally {
    try {
    provider.close();
    } catch (IOException e) {
    // Since we're done parsing and have our result, we don't care about any errors.
    }
    }
    }

    com.github.javaparser.JavaParser#CompilationUnit

    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
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    /*****************************************
    * THE JAVA LANGUAGE GRAMMAR STARTS HERE *
    *****************************************/

    /*
    * Program structuring syntax follows.
    */
    final public
    CompilationUnit CompilationUnit() throws ParseException {PackageDeclaration pakage = null;
    NodeList<ImportDeclaration> imports = emptyList();
    ImportDeclaration in = null;
    NodeList<TypeDeclaration<?>> types = emptyList();
    ModifierHolder modifier;
    TypeDeclaration<?> tn = null;
    ModuleDeclaration module = null;
    try {
    label_1:
    while (true) {
    if (jj_2_1(2)) {
    ;
    } else {
    break label_1;
    }
    jj_consume_token(SEMICOLON);
    }
    if (jj_2_2(2147483647)) {
    pakage = PackageDeclaration();
    } else {
    ;
    }
    label_2:
    while (true) {
    switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
    case ABSTRACT:
    case CLASS:
    case _DEFAULT:
    case ENUM:
    case FINAL:
    case IMPORT:
    case INTERFACE:
    case NATIVE:
    case PRIVATE:
    case PROTECTED:
    case PUBLIC:
    case STATIC:
    case STRICTFP:
    case SYNCHRONIZED:
    case TRANSIENT:
    case VOLATILE:
    case OPEN:
    case MODULE:
    case TRANSITIVE:
    case SEMICOLON:
    case AT:{
    ;
    break;
    }
    default:
    jj_la1[0] = jj_gen;
    break label_2;
    }
    switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
    case IMPORT:{
    in = ImportDeclaration();
    imports = add(imports, in);
    break;
    }
    case ABSTRACT:
    case CLASS:
    case _DEFAULT:
    case ENUM:
    case FINAL:
    case INTERFACE:
    case NATIVE:
    case PRIVATE:
    case PROTECTED:
    case PUBLIC:
    case STATIC:
    case STRICTFP:
    case SYNCHRONIZED:
    case TRANSIENT:
    case VOLATILE:
    case OPEN:
    case MODULE:
    case TRANSITIVE:
    case SEMICOLON:
    case AT:{
    modifier = Modifiers();
    switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
    case CLASS:
    case INTERFACE:{
    tn = ClassOrInterfaceDeclaration(modifier);
    types = add(types, tn);
    break;
    }
    case ENUM:{
    tn = EnumDeclaration(modifier);
    types = add(types, tn);
    break;
    }
    case AT:{
    tn = AnnotationTypeDeclaration(modifier);
    types = add(types, tn);
    break;
    }
    case OPEN:
    case MODULE:{
    module = ModuleDeclaration(modifier);
    break;
    }
    case SEMICOLON:{
    jj_consume_token(SEMICOLON);
    break;
    }
    default:
    jj_la1[1] = jj_gen;
    jj_consume_token(-1);
    throw new ParseException();
    }
    break;
    }
    default:
    jj_la1[2] = jj_gen;
    jj_consume_token(-1);
    throw new ParseException();
    }
    }
    switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
    case 0:{
    jj_consume_token(0);
    break;
    }
    case CTRL_Z:{
    jj_consume_token(CTRL_Z);
    break;
    }
    default:
    jj_la1[3] = jj_gen;
    jj_consume_token(-1);
    throw new ParseException();
    }
    return new CompilationUnit(range(token_source.getHomeToken(), token()), pakage, imports, types, module);
    } catch (ParseException e) {
    recover(EOF, e);
    final CompilationUnit compilationUnit = new CompilationUnit(range(token_source.getHomeToken(), token()), null, new NodeList<ImportDeclaration>(), new NodeList<TypeDeclaration<?>>(), null);
    compilationUnit.setParsed(UNPARSABLE);
    return compilationUnit;
    }
    }

    代码实现:https://github.com/langu-xyz/JavaMethodDeclare

    参考资料:https://www.javadoc.io/doc/com.github.javaparser

  • 利用CodeQL寻找Java Deserialization Vulnerabilities

    Github发布CodeQL后,一直保持着关注,从17年就有类似的想法在尝试,在CodeQL中有很多不谋而合的点,查询语句刚上手虽然有些别扭,稍微适应了下感觉还好,值得好好学习一下

    接下来看一下如何发现Java Deserialization Vulnerabilities。

    攻击者在Java应用 deserialization时注入不可信数据进而可以执行任意代码。

    java.io.ObjectInputStream中的 readObject是个危险方法。常见的用法如下:

    1
    2
    ObjectInputStream ois = new ObjectInputStream(input);
    MyObject obj = (MyObject)ois.readObject();

    readObject方法的作用是从数据流中读取并返回该对象。那么我们都知道当构造序列化数据时插入恶意代码,则可以在 deserialization时产生非预期结果,甚至可以执行任意代码。

    使用CodeQL发现不安全的deserialization

    我们可以使用CodeQL来发现 deserialization漏洞,当然我们首先找到 deserialization进行的位置,然后需要跟踪不可信的数据是否可以到达 deserialization调用方法。

    首选我们编写一个查询语句去寻找 readObject调用。

    1
    2
    3
    4
    5
    6
    7
    8
    import java

    from MethodAccess call, Method readobject
    where
    call.getMethod() = readobject and
    readobject.hasName("readObject") and
    readobject.getDeclaringType().hasQualifiedName("java.io", "ObjectInputStream")
    select call

    这段codeql代码的意思是寻找名称为 readObject且类型为 java.io.ObjectInputStream的方法。

    上文这段代码会返回很多结果,其中大部分都是安全的。因此我们要定位到那些可读取脏数据的调用上。进行污点跟踪主要靠 RemoteFlowSourceflowsToRemoteFlowSource的作用是发现可以由用户控制的输入点,例如http请求参数。谓词 flowsTo的作用是监控数据流是否从 source到达 sink

    首先将查询重构为一个类,来定义我们感兴趣的 sink。也就是 readObject的调用集合,这里是脏数据流入的地方。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class UnsafeDeserializationSink extends Expr {  
    UnsafeDeserializationSink() {
    exists(MethodAccess call, Method readobject |
    call.getMethod() = readobject and
    readobject.hasName("readObject") and
    readobject.getDeclaringType().hasQualifiedName("java.io", "ObjectInputStream") and
    this = call.getQualifier() )
    }
    }

    接下来我们定义 sinksource定义于 RemoteFlowSource,完整的查询语句如下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import java

    import semmle.code.java.security.DataFlow

    class UnsafeDeserializationSink extends Expr {
    UnsafeDeserializationSink() {
    exists(MethodAccess call, Method readobject |
    call.getMethod() = readobject and
    readobject.hasName("readObject") and
    readobject.getDeclaringType().hasQualifiedName("java.io", "ObjectInputStream") and
    this = call.getQualifier() )
    }
    }

    from RemoteFlowSource source, UnsafeDeserializationSink sink
    where source.flowsTo(sink)
    select source, sink

    当然,上边只查询了 java.io.ObjectInputStream.readObject这一个方法,其它反序列化框架也有类似的漏洞,例如Kryo、XmlDecoder、XStream、SnakeYaml等。

    完整的反序列化漏洞查询语句如下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import java
    import semmle.code.java.dataflow.FlowSources
    import semmle.code.java.security.UnsafeDeserialization
    import DataFlow::PathGraph

    class UnsafeDeserializationConfig extends TaintTracking::Configuration {
    UnsafeDeserializationConfig() { this = "UnsafeDeserializationConfig" }
    override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
    override predicate isSink(DataFlow::Node sink) { sink instanceof UnsafeDeserializationSink }
    }
    from DataFlow::PathNode source, DataFlow::PathNode sink, UnsafeDeserializationConfig conf
    where conf.hasFlowPath(source, sink)
    select sink.getNode().(UnsafeDeserializationSink).getMethodAccess(), source, sink, "Unsafe deserialization of $@.", source.getNode(), "user input"

    参考链接:https://lgtm.com/rules/1823453799/ https://securitylab.github.com/research/insecure-deserialization

  • 奔涌的后浪与独立思考

    从小你们就在自由探索自己的兴趣;很多人在童年就进入了不惑之年,不惑于自己喜欢什么,不喜欢什么。

    昨天《后浪》刷屏了朋友圈,笔者也转发了,被何冰老师富有感染力的声音和部分走心的文案戳中

    1
    2
    3
    自由学习一门语言,学习一门手艺,欣赏一部电影,去遥远的地方旅行。

    从小你们就在自由探索自己的兴趣;很多人在童年就进入了不惑之年,不惑于自己喜欢什么,不喜欢什么。

    不过在看过之后回想,笔者认为这只是我们生活的一个剪影,并不能代表我们这一代人。这里不讨论宣传片里的UP主们,在上一篇中有提到,这是我们该去拥抱的新事物。

    今天是青年节,不同于中秋节、端午节、情人节,这是一个精神的节日。

    我们要的不应该只是小说、电影、音乐,而是不盲从不偏执.

    建立自己完整的世界观、价值观,拥有判断、批判和独立思考的能力.

    互联网的时代,给我们这代人带来了无数的便利和机会,同时快餐式的文化也随之而来,让时间成为一个奢侈品,思考也变得弥足珍贵。

    所以就更应该对那些观点、事物、现象多问一句,这就是真相吗?这就是对的吗?在如今官本位、金本位的大势之下。

    独立思考的同时更重要的是正确思考、不偏执、不杠精,因为大多数情况下往往那就是对的,但是这种能力却是需要时时刻刻保持在生活中。

    很多人认为独立思考就是多读书,笔者却不这么认为,读书的作用更多的是让我们去了解别人的想法,从而更客观的去认识这个世界,但是思想不同于苹果,别人的思考终归是别人的,只有经过吸收、碰撞、质疑后的才会成为我们自己的,这也是我们每个人世界观构建途径的一部分(当然也可能会被不断的颠覆,不断的重构),读书更关键的在于去思考。

    希望我们这代年轻人,不再以屁民、韭菜、社畜自居,而是试着去改变去突破,无所畏惧方有无限可能。

    1
    奔涌吧,后浪

    2020.5.4 写给笔者自己

  • 通过注解/切面方式实现方法权限校验

    AOP是Spring的灵魂

    针对于垂直越权以及未授权访问等风险场景,用AOP来做权限校验是个不错的选择,可以使权限校验的代码逻辑更清晰和更具灵活性(就是有时候忘了加注解就很操蛋了,这时候建议在请求入口处做全局基础权限判断来兜底,此处不细讲该方案)。

    1. 创建注解

    创建注解,注解的作用域是METHOD

    假设该权限校验场景下的所有API都需要登陆,那我们这里默认needLogintrue

    1
    2
    3
    4
    5
    6
    7
    8
    @Target({ ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @Documented
    public @interface AutoAuth {

    boolean needLogin() default true;
    }

    2. 实现注解的过滤逻辑

    接下来就要开始实现注解的权限校验逻辑了,伪代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class AutoUserAuthAspect {

    public AuthResult checkUser(ProceedingJoinPoint pjp, AutoAuth autoAuth) throws Throwable {
    AuthResult result = null;
    Throwable error = null;
    try {
    Object[] args = pjp.getArgs();
    if (null == args || args.length == 0) {
    return autoAuth.needLogin() ?
    “Need login” :
    “Error params”;
    }
    //判断是否登陆
    //获取到session/cookie
    //解析获得userid
    //判断该userid的角色
    //判断角色的授权API范围
    //返回权限结果 true/false

    return result;
    }
    }

    接下来就是通过配置文件将处理逻辑关联到注解上,同时限定有效范围。

    application-context.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <bean id="AutoUserAuthAspect" class=“xxxxx.aspect.AutoUserAuthAspect">
    <property name="session" ref="session"/>
    </bean>
    <aop:config>
    <aop:aspect id="autoUserAccess" ref="AutoUserAuthAspect">
    <aop:around method="checkUser" pointcut="(execution(* com.xxx.xxx.xxx..*.*(..)))
    and
    (@annotation(autoAuth))”/>
    </aop:aspect>
    </aop:config>

    3. 使用注解

    在入参对象中,将其加到需要过滤的参数上,

    1
    2
    3
    4
    5
        @AutoAuth
    public Result query(){
    //业务逻辑
    }

  • 通过注解方式实现入参过滤(String类型为例)

    不使用注解的SpringBoot是没有灵魂的

    在有些场景下,需要对入参进行过滤,例如sql中不能预编译的字段等,这时候可以通过写一个过滤注解,然后只需要将需注解加到想要过滤的字段上,便可以完成预想逻辑的过滤操作。

    1. 创建注解

    @Target({ ElementType.FIELD, ElementType.PARAMETER})

    //@Target用于设定注解使用范围

    //Target通过ElementType来指定注解可使用范围的枚举集合

    //FIELD: 可用于域上

    //PARAMETER:可用于参数上

    @Retention(RetentionPolicy.RUNTIME)

    //@Retention注解可以用来修饰其他注解,是注解的注解,称为元注解。

    //@Retention注解有一个属性value,是RetentionPolicy类型的。

    //RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,JVM加载class文件之后,仍然存在

    @Constraint(validatedBy = {ParamsConstraintValidator.class})

    //自定义验证注解

    这段的逻辑是CheckParams注解加工的默认类型是String,返回消息默认Param Illegal,注解的具体过滤逻辑在ParamsConstraintValidator类中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    /**
    * 对入参字段做sql注入风险过滤,String类型为默认配置类型
    */

    @Target({ ElementType.FIELD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = {ParamsConstraintValidator.class})

    public @interface CheckParams {

    boolean required() default true;

    String message() default “Param Illegal";

    /**
    * type:String
    * @return
    */
    String type() default “String";

    }

    2. 实现注解的过滤逻辑

    自定义注解处理类必须实现ConstraintValidator接口, 其中CheckParams是自己定义的注解, 而Object是注解标注参数的类型,也可以是String或者其它类型。

    typemessage在上边已经默认配置,在添加注解时可以进行覆盖。

    SecUtil.escapeSql是过滤参数的方法,这里不细写了,任何我们想要的参数加工逻辑都可以。

    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
    public class ParamsConstraintValidator implements ConstraintValidator<CheckParams, Object> {

    public static final String STRING_TYPE = “String";
    private String type = "";
    private String message ="";

    @Override
    public void initialize(CheckParams constraintAnnotation) {
    ConstraintValidator.super.initialize(constraintAnnotation);
    type = constraintAnnotation.type();
    message = constraintAnnotation.message();
    }

    @Override
    public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
    if (o == null) {
    return true;
    }
    constraintValidatorContext.disableDefaultConstraintViolation();
    constraintValidatorContext.buildConstraintViolationWithTemplate(message).addConstraintViolation();
    switch (type) {
    case STRING_TYPE:
    return checkString(o);
    default:
    return true;
    }

    }

    /**
    * 检验String,List<String>
    *
    * @param o
    * @return
    */
    private boolean checkString(Object o) {
    if (o instanceof String) {
    String obj = (String)o;
    String afterObj = SecUtil.escapeSql(obj);
    return Objects.equals(obj, afterObj);
    } else if (o instanceof List) {
    List list = (List)o;
    for (Object it : list) {
    if (it instanceof String) {
    String obj = (String)it;
    String afterObj = SecUtil.escapeSql(obj);
    if (!Objects.equals(obj, afterObj)) {
    return false;
    }
    }
    }

    }
    return true;
    }
    }

    3. 使用注解

    在入参对象中,将其加到需要过滤的参数上,

    1
    2
    3
    4
    5
    @CheckParams(message = “Param Illegal")
    private List<String> param1;

    @CheckParams(type = “String”, message = “Param Illegal")
    private String param2;
  • 为什么我们会对新事物产生抵触心理

    当我们做出选择改变时,不知道将会得到什么,但很清楚将会失去什么。

    回想一下,是不是或多或少会有一些类似的场景,当出现一个新的偶像明星时,特别是帅的或漂亮的,会迅速的下一个判断,觉得没有真才实学全靠包装,觉得他们的粉丝都是脑残;在玩LOL或王者时,被对面强势英雄干的很惨,是不是每次先把自己觉得打不过的ban掉,却很少去主动练习这类强势英雄;当一项新技术、新设计、新政策出现时,网上和我们身边,往往会充斥着吐槽、抵制的声音,往往要很长一段时间才会变成真香,例如当年汽车诞生时的《红旗法案》。

    上述的这类例子数不胜数,笔者有时候会觉得自己是不是没有跟上这个时代的节奏。所以将这种下意识的心理活动拿出来分析下,一家之言,不必当真。

    认识的一位博学大佬,在分析人的行为时,经常会用进化论来分析,学到很多。细想了下,这种抵触心理一样可以用进化论来解释,在人类茹毛饮血的时代,危机四伏,尝试新事物往往意味着未知的风险,而不去尝试则风险会小很多,所以对未知的恐惧就写入了我们的基因里,一直到今天。

    从人的内心来看,接受新的事物,本质是和旧的自己对抗,而与自己做斗争往往是最难的。这个旧的自己,我把它理解为成见,在《哪吒之魔童降世》里有句话感触很深:“人心中的成见是一座大山,任你怎么努力都休想搬动。”

    但从笔者自身来看,却不是对所有的新事物都会产生抵触心理,在那些自己比较熟悉的领域,往往会有相反的状态,努力去学习最前沿的技术,希望出现使生产力更高的事物。这么看来,我们对新事物的抵触往往是出现在那些陌生或者简单了解的领域,而在这些领域,经常会出现轻视的情况,对自己简单了解的事情轻易的做出判断,在韩寒的《我也曾对那种力量一无所知》一文中详尽道出。

    在想明白了这些后,也就理解了为什么还有那么多人在吐槽“拥抱变化”这条价值观,拥抱变化说到底也是让我们去积极拥抱新事物。因为当我们做出选择改变时,不知道将会得到什么,但很清楚将会失去什么。

    古有“昔有学步于邯郸者,曾未得其仿佛,又复失其故步,遂匍匐而归耳。” 近有“穷则变,变则通”。有个寓言故事《谁动了我的奶酪?》,说的也是这个道理。

    在这段时间里,笔者尝试着去改变,在面对新事物的时候,下意识的告诉自己去试一试,说不定是新的世界。当抖音、头条兴起的时候,在简单的使用过后,武断的下了“没有营养的APP”的结论,但是就是这样的业务,火遍全球,成为国内互联网公司出海比较成功的一个,背后的价值需要去深入思考(当然笔者现在还是觉得比较浪费时间),但是短视频、vlog等新的信息载体要尝试主动去拥抱。还有AI、5G等等,Elon Musk的风格是值得学习的。

    生活中也有一些尝试,听歌的时候也会点开“今日流行”歌单盲听;在股市、币市里投了一点钱尝试去感受这个领域的变化;也有尝试着去融入00后的圈子,感受时代的变化,等等。

    ~~注:本文是笔者睡前随笔,不带有任何观点