# 安全开发规范
# Web安全漏洞和防御
以下列举了Web系统常见的安全漏洞,并非简单从网上摘抄片段,而是结合V5标准产品实际情况给出了具体解决方案。比如SQL注入、XXE、XSS、CSRF都是开发V5代码过程中常见的漏洞BUG,所有开发务必熟读并灵活运用。
而失效的身份认证和失效的访问控制则是无形中的安全问题,极易产生0Day高危,同样需要开发扎实的功底和敏锐的安全开发嗅觉。
# 一、SQL注入
SQL注入就是指Web应用程序对用户输入的数据的合法性没有进行判断,前端传入后端的参数使攻击者可控的,并且参数中带有数据库查询,攻击者可以通过构造不同的SQL语句来实现对数据库的任意操作。
SQL注入漏洞产生需要以下两个条件:
- 参数是用户可控的:前端传给后端的参数内容是用户可以控制的
- 参数带入数据库查询:传入的参数可以带入数据库中查询
# 攻击方法
String taskId = request.getParameter("taskId");
// Dangerous code
String sql = "SELECT ID,SUBJECT from task_info where ID="+taskId;
List result = executeSQL(sql);
以上代码是最常见的SQL注入案例,从代码写法上能看出:我们从客户端获得了一个taskId参数,然后将此参数作为条件放入SQL执行数据库查询。
攻击者可以自己组装客户端的请求参数,比如将taskID的值修改为:taskID=123 or id is not null,最终会执行如下SQL,显而易见:此时后台被SQL注入攻击了,此时攻击者能拿到该表的全部数据。
// 带有攻击的SQL,SQL注入:
SELECT ID,SUBJECT from task_info where ID = 123 or id is not null
# 防御方法
防护SQL注入非常简单,通俗说:所有SQL、HQL中Where条件查询都使用?、:param占位符的形式编写,不要直接使用 "column="+param这种SQL硬拼接方式。使用Mytabis 框架时,用#{}代替${}进行参数化查询。
专业说法就是:使用预编译SQL执行查询。
String taskId = request.getParameter("taskId");
// safer code
String sql = "SELECT ID,SUBJECT from task_info where ID= :id";
Map<String, Object> params = new HashMap<>();
params.put("id", Long.valueOf(taskId));
扩展:标准产品提供了SQL注入的检测手段:通过系统管理员>系统监控>底部按钮HQL Dump>打开链接即可看到当前系统是否存在SQL注入。
# 二、失效的身份认证
失效的身份认证,也叫冒充他人身份,常见场景为非法获得他人帐号权限,从而可以利用他人帐号身份进行非法操作。
危害:这些漏洞可能导致部分甚至全部账户遭受攻击,一旦攻击成功,攻击者就能执行合法的任何操作。
# 攻击和防御方法
攻击方式有很多,这里列举几个常见的:
1)弱口令
攻击方法:比如给所有系统用户默认密码123456,攻击者就可以猜到用户名的前提下,直接输入123456访问用户帐号。
防御方法:通过配置或修改代码增强密码强度,不允许用户设置密码为弱口令
2)内置固定帐号
攻击方法:系统为了简化管理员工作量,在安装系统时内置了固定的管理员帐号,如果管理员给帐号设置成弱口令,攻击者则可以很容易登录系统。
标准产品集成帆软8、9时遇到过帆软的固定管理员漏洞:如果首次访问帆软后台系统,需要设置管理员密码。这样导致,万一有用户没用过帆软后台系统,而攻击者成了首次访问者,攻击者就可以录入自己的密码登录,进行攻击。
防御方法:1、管理员密码在安装程序安装过程中设置,永远不内置固定帐号;2)用户密码不允许首次访问时设置,而必须在创建帐号时由用户录入与之绑定的高强度密码;3)每套系统的密钥随机生成并存放在安全的位置。
3)找回密码机制太弱
攻击方法:如果找回密码的认证机制太过简单,比如只需要输入帐号+手机号,不用获取手机验证码就可以找回的话,会导致攻击者在知道用户基本隐私信息下通过找回密码让帐号强制易主。
防御方法:增强找回密码验证机制,通过手机号获取短信验证码找回,通过邮箱确认链接找回。
4)极简的单点登录认证
单点登录是一种特殊的认证方式,通常通过URL中携带一个ticket参数,此参数与系统中的某个帐号身份对应,当系统单点登录认证通过后就会给予对应帐号相同的权限。单点登录这个ticket需要双方系统通过编码进行约定,如果ticket过于简单就容易被攻击者仿照模拟接管权限。
攻击方法:某些集成开发为了省事将ticket设置为用户的帐号,或者对帐号进行简单编码就当作ticket,这种方式都是错误的编码认证方式。
// Dangerous code
URL?ticket=system
// Dangerous code
URL?ticket=base64('system')
上面这种单点登录的ticket都是不安全的认证策略,攻击者只要猜到用户帐号就可以模拟相同的URL访问系统。
防御方法:产生和管理ticket的源头代码一定要强大且安全:保障ticket随机、带时效性!比如给对应帐号生成UUID的ticket,并且用户退出时自动销毁ticket或只允许ticket有xx分钟有效期。
/**下面是一段相对规范的SSO认证示例,本例是用户登录OA的前提下访问第三方系统前,由OA给第三方系统颁发证书的代码**/
String loginName = AppContext.currentUserLoginName();
// 从SSO认证中心查询当前登录人的ticket是否已经存在
TicketInfo ticketInfo = SSOTicketManager.getInstance().getTicketInfoList(loginName,"biinside");
if(ticketInfo == null){
// 如果不存ticket,则自己注册一个ticket,推荐ticket是随机数
String newTicket = String.valueOf(UUIDLong.absLongUUID());
SSOTicketManager.getInstance().newTicketInfoList(newTicket, loginName, "biinside");
// 与第三方通信,将当前ticket传输过去认证
String sso = preUrl + "/api/Portal/A8SSO/?ticket="+newTicket;
}
# 三、敏感数据泄露
近年来,敏感数据泄露已经成为了一最常见、最具影响力的攻击,不久前就爆出过Facebook泄露了用户的大量信息,以及12306也多次泄露用户的信息。现在信息泄露已经成为了owasp top 10中的漏洞,可想而知敏感信息泄露现在已经成为十分严重的问题。
# 攻击方法
攻击者不是直接攻击密码,而是在传输过程中或从客户端窃取密钥、发起中间人攻击,或从服务器端窃取明文数据,还有可能由于管理员的安全性不高,使用弱密码,被攻击者暴力破解,进入到数据库拿到敏感信息。
# 防御方法
- 不要在错误页面中泄露敏感信息,如系统详细信息、会话标识符、用户账号信息、系统物理路径、数据库路径、SQL 语句等,正确配置错误回显界面。
- 建议删除 JavaScript 的注释代码,避免注释代码中存在遗留的测试账号信息、敏感接口地址、以及第三方服务的 access_key 等敏感信息造成泄露。
- 避免在前端代码中存放敏感信息,如硬编码加密秘钥、Hidden 字段存放管理员账号密码等。
- 禁止使用GET 方法传递敏感参数(会话标识、身份证号等),因为GET方法会将数据显示在URL中,传输过程中所有的代理及缓存服务器都可以直接获取用户数据。或者在传输过程中经过脱敏处理。为所有敏感信息采用加密传输,并且确保加密协议为安全版本。
- 不要在日志中保存敏感信息,如会话ID、用户账号信息等。
- 密码类数据信息必须采用SHA-256、SM3、MD5+Salt 的方式进行存储。
- 需要逆向的敏感信息(如身份证号、银行卡号等)在存储时建议使用高强度的加密算法(如 AES-128、SM4)进行保护。
- 禁止带有敏感数据的 Web 页面缓存。
开发层面能做的事情为:
1)对敏感数据进行加密存放,比如用户的手机号、邮箱、地址存储到数据库均采用AES-128、SM4这类可逆加密;对于用户的密码则采用SHA-256、SM3摘要算法;对于附件采用SHA-256、SM3进行签名认证。平台提供了完整的加解密API方案,详见CTP技术平台>加密解密相关内容。
//fileManager加密及验证签名示例
//致远V8.1之后提供了多种加密算法选择(国际标准SHA-256和AES-128、国标标准SM3和SM4、外接加密机),具体选用什么通过系统管理员配置完成
if (Strings.equals(Boolean.TRUE.toString(), isEncrypt)) {
//获取加密coder工厂,工厂类会根据系统管理员配置选择对应算法
EncryptCoderFactory coderFactory = EncryptCoderFactory.getInstance();
//传入加密操作ENCRYPT_ATTACHMENT(文件)
EncryptCoder encryptCoder = coderFactory.getByEncryptActionEnum(EncryptActionEnum.ENCRYPT_ATTACHMENT);
//调用encrypt方法加密数据
fileBytes = encryptCoder.encrypt(fileBytes);
//签名依赖系统配置
if (EncryptCoderFactory.enableSignature()) {
//生成签名信息
EncryptCoder signatureCoder = coderFactory.getByEncryptActionEnum(EncryptActionEnum.SIGNATURE_DATA);
signature = signatureCoder.signature(fileBytes);
}
}
对于特殊需求场景,你也可以直接通过枚举选择对应加密算法加密:
// 使用系统内置SM3加密,其它加密算法参考EncryptAlgorithmEnum枚举即可,起始于V8.1版本
EncryptAlgorithmEnum.SYS_SM3.getEncryptCoder().encrypt(data);
2)不要将数据SQL错误返回给前端,虽然看上去给开发省了很多排查问题的时间,但实际这是严重的安全漏洞,近期已经有多家客户上报客户BUG:
public void result(HttpServletRequest request,HttpServletResponse response) throws IOException {
try {
taskManager.countChildTaskInfo(taskId);
} catch (BusinessException e) {
// Bad code:不要把SQL异常输出到前端,正确的做法是log.error("",e)记录异常到日志文件中
response.getWriter().write("出现SQL异常,SQL为:"+e.getLocalizedMessage());
}
}
3)页面注释问题:Javascript注释、HTML注释在前端都可以通过“查看源代码”被发现,而JSP页面的JSP注释不会显示在前端页面。我们允许添加注释,但请务必保障注释不要泄露重要信息!
<body>
<!-- 这段HTML注释会被调试源代码扫描出来 -->
<%-- 这段JSP注释,页面调试时不会被扫描出来 --%>
<input name="">
</body>
4)严禁将敏感信息输出在Log日志:
// Bad Code:严禁将用户机密信息输出到日志
log.info("当前用户密码:"+AppContext.getCurrentUser().getPassword());
log.info("当前用户手机号:"+xxx.getPhone());
log.info("当前用户邮箱:"+xxx.getEmail());
5)严禁将所有信息输出在控制台:
try {
insertLog4Account(user, user.getLoginAccount(), actionId, params);
} catch (Exception e) {
// Bad Code:异常会被输出在控制台,并且无法追踪是什么类输出的
e.printStackTrace();
// Bad Code:这样的写法同样会将日志输出在控制台,这是不允许的
System.out.println(e);
// Nice Code:将日志放入log4j管理是正确的处理方式
log.error("", e);
}
7)敏感信息放到header、body中,并以HTTPS形式发送Request,以提升URL安全性。
# 四、XML外部实体(XXE)
XXE(XML External Entity)指的是XML外部实体注入,XML用于标识电子文件使其具有结构性的标识语言,可以用来标记数据、定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言。XML文档结构包括XML声名、DTD文档类型定义、文档元素。
# 攻击方法
如果攻击者可以上传XML文档或者在XML文档中添加恶意内容,通过易受攻击的代码、依赖项或集成,他们就能够攻击含有缺陷的XML处理器。XXE缺陷可用于提取数据、执行远程服务器请求、扫描内部系统、执行拒绝服务攻击和其他攻击。
# 防御方法
1)尽可能使用简单的数据格式(如JSON)替代XML,避免对敏感数据进行序列化。
2)过滤用户提交的XML数据,防止出现非法内容。平台提供了标准的工具类实现XXE防护,我们要求所有XML解析均使用XXEUtil进行一次解析,默认推荐使用Dom4j作为XML的解析器:
String xmlStr = xmlParam;
// 正确的解析XML写法
org.dom4j.Document doc=com.seeyon.ctp.util.XXEUtil.safeParseText(xmlStr);
org.dom4j.Element root=doc.getRootElement();
注意平台禁止使用XStream框架进行XML与POJO互转,XStream曾爆出多处高危漏洞。
# 五、失效的访问控制
失效的访问控制,也叫越权,指的是在未对通过身份验证的用户,实施恰当的访问控制。攻击者可以利用这一漏洞,访问未经授权的功能或数据。比如,访问其他用户的账户、查看敏感文件、修改其他用户的数据,更改访问权限等等,都属于失效的访问控制造成的后果。
# 攻击和防御方法
1)最常见的是水平越权和垂直越权。比如URL?method=saveUser这个URL请求原本设计只有管理员能使用,但开发人员忘记对这个URL进行权限校验,普通用户则可以复制这个URL,模拟对应参数进行操作。
平台提供了CheckRoleAccess注解、编码级权限控制、DigestURL等方案来进行权限控制,开发过程中需要根据不同场景实现对应权限控制。更多信息详见CTP技术平台>安全访问控制相关内容。
/**
* 协同模版首页访问页面:通过注解控制权限
*/
@CheckRoleAccess(resourceCode = "F01_colltemplate",roleTypes = { Role_NAME.TtempletManager})
public ModelAndView indexFrame(HttpServletRequest request,HttpServletResponse response) throws Exception {
request.setAttribute("isAccountAdmin", collaborationTemplateManager.isAccountAdmin());
return new ModelAndView("common/template/indexFrame");
}
2)免登录URL:通常管理系统都需要登录之后才能使用,带登录身份才能保障系统安全,但某些页面决定了无需登录也可以请求,比如登录、找回密码,这些请求就是免登录URL。
首先,所有免登录URL都是有风险的,因为服务一旦部署到互联网,免登录URL就有可能受到第三方攻击,所以平台要求所有二次开发以及新功能都不允许新增免登录URL。
3)路径遍历攻击是极其危险的攻击方式:攻击者通过URL./ …/这种路径遍历方式进行攻击,等于原本访问的是A目录,通过路径遍历攻击者能访问A的上一层别的目录。
场景一:“省事”的MVC跳板。
下面是一个Controller,开发提供了一个万能方法:前端传入页面名称,后端通过此跳板自动重定向到对应页面。
public class DoEverythingController extends BaseController {
public ModelAndView toAnyWhere(HttpServletRequest httpRequest, HttpServletResponse httpResponse){
String jspName = httpRequest.getParameter("jspName");
ModelAndView mav = new ModelAndView("apps/jsp/task/"+jspName);
return mav;
}
}
看起来,开发的实现很美好,像如下的请求都能被重定向到正确的位置:
doEverythingController.do?mehotd=toAnyWhere&jspName=page1=>apps/jsp/task/page1.jsp
doEverythingController.do?mehotd=toAnyWhere&jspName=page2=>apps/jsp/task/page2.jsp
doEverythingController.do?mehotd=toAnyWhere&jspName=page3=>apps/jsp/task/page3.jsp
但开发是否想过如果有人不按常规出牌会怎么样?下面这种请求就出现了“失效的访问控制”,攻击者通过此请求访问到了别的目录JSP!
doEverythingController.do?mehotd=toAnyWhere&jspName=../system/monitor=>apps/jsp/system/monitor.jsp
解决方法是:建立白名单,只有列表中的页面才允许访问:
public class DoEverythingController extends BaseController {
String[] whiteList = new String[]{"page1","page2","page3"};
public ModelAndView toAnyWhere(HttpServletRequest httpRequest, HttpServletResponse httpResponse){
String jspName = httpRequest.getParameter("jspName");
if(!ArrayUtils.contains(whiteList,jspName)){
return null;
}
ModelAndView mav = new ModelAndView("apps/jsp/task/"+jspName);
return mav;
}
}
场景二:任意文件下载。
如下代码是一个文件下载的实现,开发人员的初衷是:前端传给我文件名,我再根据文件名去下载特定目录下的指定文件。
public class DownloadController extends BaseController {
public ModelAndView download(HttpServletRequest httpRequest, HttpServletResponse httpResponse){
String fileName = httpRequest.getParameter("fileName");
String parentFile = SystemEnvironment.getBaseFolder() + File.separator + "upload";
File targetFile = new File(parentFile + File.separator + fileName);
return downloadFile(targetFile);
}
}
看起来,开发的实现很美好,像如下的请求都能按预期正常下载:
downloadController.do?method=download&fileName=zhixin.exe=>对应base/upload/zhixin.exe
downloadController.do?method=download&fileName=office/iweboffice.zip=>对应base/upload/office/iweboffice.zip
但开发是否想过如果有人不按常规出牌会怎么样?下面这种请求就出现了“失效的访问控制”,攻击者通过此请求能下载任意目录的文件!
任意文件下载是高危漏洞,攻击者如入无人之境,可以遍历下载服务器所有资料。
downloadController.do?method=download&fileName=../conf/datasourceCtp.properties=>对应base/conf/datasourceCtp.properties
downloadController.do?method=download&fileName=~/xxx=>Linux系统对应root或home根目录下的文件
解决方法为两种:
- 第一种:判断下载文件路径是否在指定父目录下
- 第二种:禁用前端动态传入文件名的下载方式,该为根据文件ID下载
public class DownloadController extends BaseController {
public ModelAndView download(HttpServletRequest httpRequest, HttpServletResponse httpResponse){
String fileName = httpRequest.getParameter("fileName");
String parentFile = SystemEnvironment.getBaseFolder() + File.separator + "upload";
File targetFile = new File(parentFile + File.separator + fileName);
// 第一种:核心判断
if(!FileUtil.inDirectory(targetFile, new File(parentFile))) {
// 如果targetFile绝对路径不在parentFile父目录下则不允许下载
return null;
}
return downloadFile(targetFile);
}
}
public class DownloadController extends BaseController {
public ModelAndView download(HttpServletRequest httpRequest, HttpServletResponse httpResponse){
// 第二种:不信任前端的文件名,用UUID的fileID替代,从数据库查找映射文件
String fileId = httpRequest.getParameter("fileId");
String parentFile = SystemEnvironment.getBaseFolder() + File.separator + "upload";
File targetFile = findFileById(fileId);
return downloadFile(targetFile);
}
}
# 六、安全配置错误
安全配置错误是最常见的安全问题,这通常是由于不安全的默认配置、不完整的临时配置、开源云 存储、错误的HTTP 标头配置以及包含敏感信息的详细错误信息所造成的。因此,我们不仅需要对所 有的操作系统、框架、库和应用程序进行安全配置,而且必须及时修补和升级它们。
通常,攻击者能够通过未修复的漏洞、访问默认账户、不再使用的页面、未受保护的文件和目录等来取得对系统的未授权的访问或了解。
# 攻击方法
安全配置错误可以发生在一个应用程序堆栈的任何层 面,包括网络服务、平台、Web服务器、应用服务器、数据库、框架、自定义代码和预安装的虚拟机、容器 和存储。自动扫描器可用于检测错误的安全配置、默认帐户的使用或配置、不必要的服务、遗留选项等。
比如,客户所使用的服务器操作系统有高危漏洞并且对外暴露了大量端口号,攻击者可以利用指定端口利用操作系统高危漏洞接管服务器,随后进行非法行动。
同样,系统中使用的一些框架被爆有高危漏洞(如Log4j漏洞、Struts漏洞),客户未及时修复,攻击者可以利用这些框架进行攻击。
# 防御方法
防御方法大多与开发过程代码无关,更多是系统级、架构级、运维级的安全升级处理方案:
- 保证所有组件都是最新版本,并具有适当的安全配置,包括删除不需要的配置和文件夹,关闭或屏蔽不必要的端口,更改默认口令。如 fastjson、jackson 升级至最新安全版本。
- 对应用系统所在服务器,所使用的框架和第三方库进行安全配置。如 Spring BootActuator 配置不当导致的高危漏洞,Tomcat AJP 文件包含漏洞等。
- 验证应用程序资源是否被托管,例如 javascript 库、css 样式表、Web 字体由应用程序托管,而不是依赖于 CDN 或外部提供者。
- 删除 web 目录下存在敏感信息的备份文件、测试文件、临时文件、旧版本文件等。
- 产品不能运行在开发和 Debug 模式。
- 当前在用的操作系统没有已知的漏洞。
- 禁止启动不用的服务,例如,FTP、Telnet、SMTP 等。
- 启动应用程序的系统用户必须是专用的、没有系统级别特权的用户和组。
- 在部署之前,删除没有用的功能和测试代码。
# 七、跨站脚本攻击(XSS)
xss攻击全称为跨站脚本攻击,当应用程序的新网页中包含不受信任的、未经恰当验证或转义的数据时,或者使用可以创建HTML或JavaScript 的浏览器API 更新现有的网页时,就会出现XSS 缺陷。XSS 让攻击者能够在受害者的浏览器中执行脚本,并劫持用户会话、破坏网站或将用户重定向到恶意站点。
危害:攻击者在受害者浏览器中执行脚本以劫持用户会话,插入恶意内容,重定向用户,使用恶意软件劫持用户浏览器等。
种类:存储型,反射型,DOM型
如何防范:
- 验证输入:产品层面控制文本框的特殊字符比如<>这种字符禁止输入
- 编码输出(用来确保输入的字符被视为数据,而不是作为html被浏览器所解析)
# 防御方法
1)JSP中少用或者不用EL表达式,这样能极大减少XSS攻击。比如URL中的参数变量,不要使用EL表达式来获取,而是通过javascript解析参数变量:
<script text="text/javascript">
// 格式如下的请求不要用EL表达式获取参数 xx.do?method=xx&id=xx&name=xx&other=xx
var id = "${param.id}"; // Bad Code 可能存在反射型注入
var name = "{param.name}"; // Bad Code 可能存在反射型注入
var queryParams = getQueryParams(); // Nice Code物理解析参数,防止XSS攻击
var id = queryParams.id;
var name = queryParams.name;
</script>
<script text="text/javascript">
/**
* 获取url后边的参数实现原理 注:此方法平台已经封装好,业务组调用即可
* @returns
*/
function getQueryParams(){
var search = location.search;
var param = {};
if (search.length > 0) {
var strs = search.split("?")[1].split("&");
for (var i = 0; i < strs.length; i++) {
var con = strs[i].split("=");
param[con[0]] = decodeURIComponent(con[1]);
}
}
return param;
}
</script>
后端MVC框架Controller层只做路径跳转,ModelAndView中不携带任何参数。
// 推荐:Controller只做路由跳转,在前端JSP页面通过AJAX获取数据能极大避免XSS攻击
public ModelAndView agendaList(HttpServletRequest httpRequest, HttpServletResponse httpResponse){
ModelAndView modelAndView = new ModelAndView("apps/leaderagenda/agendaList");
return modelAndView;
}
// 不推荐:mav携带Object给前端
public ModelAndView agendaList(HttpServletRequest httpRequest, HttpServletResponse httpResponse){
ModelAndView modelAndView = new ModelAndView("apps/leaderagenda/agendaList");
// 不推荐携带参数变量
modelAndView.addObject("userName",AppContext.currentUserName());
modelAndView.addObject("accountName",AppContext.currentAccountName());
return modelAndView;
}
<!-- 接上面代码示例,如果前端JSP通过EL表达式接受ModelAndView参数,恰巧userName有特殊字符就会造成存储型XSS攻击 -->
<input type="text" value = "${userName}">
<input type="text" value = "${accountName}";
2)历史代码,若JSP的script标签块内已经有EL表达式且存在XSS攻击,则需要使用平台的${ctp:escapeJavascript(x)}标签进行转换:
<!-- JSP脚本区域使用ctp:escapeJavascript,如果是老代码,还没有ctp标签,需要使用v3x:escapeJavascript实现 -->
<script type="text/javascript">
var _resourceCode = "${ctp:escapeJavascript(param._resourceCode)}";
</script>
历史代码,若JSP中html dom value attribute元素存在EL表达式且存在XSS攻击,需要使用${fn:escapeXml(x)}进行转换:
<!-- dom中需要使用fn:escapeXml -->
<input type="hidden" id="app" name="app" value="${fn:escapeXml(param.app)}">
历史代码,若JSP中DOM标签块内存在EL表达式且存在XSS攻击,需要使用${ctp:toHTML(x)}进行转换:
<!-- 如果是老代码,还没有ctp标签,则需要改为${v3x:toHtml(x)} -->
<tr>
<td>${ctp:toHTML(param._resourceCode)}</td>
</tr>
<!-- 如下c:out标签等同上面的写法 -->
<tr>
<td><c:out value="${username}" escapeXml="true"></c:out></td>
</tr>
3)如无必要不要在后端代码进行转换:这些数据在后端本身是无害的,只是在前端渲染才有害,而且渲染在script区、dom属性值区、dom内容区的XSS防护方法是不同的,后端一个参数被用在多处则无法面面俱到。同时如果这个后端代码只是被用着远程接口调用,通过escape会将本应正确的数据转换错误,这是不合理的。
如果当前代码耦合过于严重,无法在前端处理,则后端转换是最后的策略,后端转换也需要根据实际情况判定到底是基于Javascript转换还是html转换:
org.springframework.web.util.JavaScriptUtils.javaScriptEscape(xssStr);
org.springframework.web.util.HtmlUtils.htmlEscape(xssStr);
4)不要将后端异常抛出到前台页面,如果前端没有做好防护,极易引发XSS攻击。
如下是真实攻击案例:攻击者拦截了我们的URL请求,原本memberID参数应该是一个标准数字类型,但是攻击者将其篡改为XSS攻击脚本,最后异常被返回到前端页面,前端未及时防护导致XSS攻击生效。
// request URL: xxx?memberId=<scirpt>alert(1)</scirpt>
try {
Long memberId = Long.valueOf(request.getParameter("memberId"));
} catch (Exception e) {
// 前端将输出:For input string: "For input string: "<scirpt>alert(1)</scirpt>""
response.getWriter().write(e.getLocalizedMessage()); // Bad code
response.getWriter().write("数据解析出现异常!"); // Nice code
logger.error("",e); // Nice code
}
5)对于Html纯前端或去EL表达式的JSP前端代码发生XSS几率会小很多,但也不是没有。
在编码过程,养成良好的习惯,保障纯前端渲染的过程:
- 浏览器先加载一个静态 HTML,此 HTML 中不包含任何跟业务相关的数据。
- 然后浏览器执行 HTML 中的 JavaScript。
- JavaScript 通过 Ajax 加载业务数据,调用 DOM API 更新到页面上。
在纯前端渲染中,我们会明确的告诉浏览器:下面要设置的内容是文本(.innerText
),还是属性(.setAttribute
),还是样式(.style
)等等。浏览器不会被轻易的被欺骗,执行预期外的代码了。
但纯前端渲染还需注意避免 DOM 型 XSS 漏洞(例如 onload
事件和 href
中的 javascript:xxx
等,请参考下文”预防 DOM 型 XSS 攻击“部分)。在很多内部、管理系统中,采用纯前端渲染是非常合适的。
DOM 型 XSS 攻击,实际上就是网站前端 JavaScript 代码本身不够严谨,把不可信的数据当作代码执行了。
在使用 .innerHTML
、.outerHTML
、document.write()
时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用 .textContent
、.setAttribute()
等。
如果用 Vue/React 技术栈,并且不使用 v-html
/dangerouslySetInnerHTML
功能,就在前端 render 阶段避免 innerHTML
、outerHTML
的 XSS 隐患。
DOM 中的内联事件监听器,如 location
、onclick
、onerror
、onload
、onmouseover
等,<a>
标签的 href
属性,JavaScript 的 eval()
、setTimeout()
、setInterval()
等,都能把字符串作为代码运行。如果不可信的数据拼接到字符串中传递给这些 API,很容易产生安全隐患,请务必避免。
作者:美团技术团队 链接:https://www.jianshu.com/p/2d9da4490ae1 来源:简书 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
# 八、使用含有已知漏洞的组件
组件(例如:库、框架和其他软件模块)拥有和应用程序相同的权限。如果应用程序中含有已知漏洞的组件被攻击者利用,可能会造成严重的数据丢失或服务器接管。同时,使用含有已知漏洞的组件的应用程序和API可能会破坏应用程序防御、造成各种攻击并产生严重影响。
# 防御方法
1.识别正在使用的组件和版本,包括所有的依赖
2.更新组件或引用的库文件到最新
3.建立安全策略来管理组件的使用
对于标准产品,开发规范中有明令禁止使用的组件库:XStream和Fastjson(两个框架历史上出现过多次高危漏洞,使用这类框架容易被白帽子盯上),(为避免不必要的麻烦)取而代之的是XXEUti+Dom4j和平台的JSONUtil。
# 九、不足的日志记录和监控
这个和等保有一定的关系,不足的日志记录和监控,以及事件响应缺失或无效的集成,使攻击者能够进一步攻击系统、保持持续性或转向更多系统,以及篡改、提取或销毁数据。大多数缺陷研究显示,缺陷被检测出的时间超过200天,且通常通过外部检测方检测,而不是通过内部流程或监控检测。
# 防御方法
这类问题的防护更多是基于国家等保分保标准进行相应的产品合规性开发,标准产品尽量按照三级标准进行了相应的日志记录,具体见标准产品发布文档--安全合规专题。
# 十、不安全的反序列化
不安全的反序列化会导致远程代码执行。即使反序列化缺陷不会导致远程代码执行,攻击者也可以利用它们来执行攻击,包括:重播攻击、注入攻击和特权升级攻击。
序列化就是将对象转换成字节流,可以更方便的将数据保存到本地文件
反序列化就是将字节流还原成对象,Java中提供了两个接口来支持序列化,ObjectIutputStream()和ObjectOutputStream()
序列化相对安全,问题出在反序列化的过程
原理:攻击者通过构造恶意的参数,使数据在在反序列化后生成特殊的对象类型,从而执行恶意代码。问题的根源在于,反序列化时没有对生成的对象类型做限制
防御:反序列化漏洞的防御主要以白名单为主,限制对象的类型,从而减小影响。
# 十一、跨站请求伪造CSRF
CSRF(Cross-site request forgery),中文名称跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。
你这可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账等等,从而造成个人隐私泄露以及财产安全问题。
CSRF利用的是同一浏览器保留用户状态的特性,在主系统保持登录状态的前提下,使用同一浏览器从危险网站点击链接执行主系统的请求,最终形成CSRF跨站请求伪造。
# 攻击方法
通俗说,同一浏览器有保持会话的能力,我们在日常工作中应该体会到了:
1、先正常访问协同系统,正常输入帐号密码登录到系统个人空间
2、打开一个待办协同,复制一下浏览器上待办协同的URL地址
3、关闭当前待办协同的窗口,不关闭个人空间主浏览器,再新建一个标签页(Ctrl+T)打开一个空白页,输入第2步复制的待办协同URL地址回车
4、按照第3步操作之后,你会发现一个空白页签,输入协同内的地址也能正常打开页面
CSRF就是基于上面浏览器原理实现的攻击,攻击场景为:
1、原本客户有一个正常的办公主系统,网站地址为:xxx.seeyon.com,首先受害者正常登录此地址进入系统。
2、在这个正常的办公系统有一个URL是用于给系统内人员点赞,点赞高的将会获得公司的奖励,链接地址为,xxx.seeyon.com/xxx/likes,正常登录办公主系统即可给人员点赞。
3、别有用心之人如果想无声无息获得高赞,可以利用CSRF漏洞:攻击者可以开发另一个危险的网站,比如aaa.com,该网站首页有一个按钮名叫“点击我获得1万元”,而这个按钮的链接实际是:xxx.seeyon.com/xxx/likes
4、攻击者只要利用受害者先保持主系统在线,然后引导受害者在同一浏览器访问危险网站aaa.com去点击“点击我获得1万元”按钮即可立刻获得主系统的点赞。
# 防御方法
防护CSRF的攻击就是要让想办法让“危险网站”直接请求主系统URL失效。
V5标准产品提供了在URL后添加CSRFTOKEN的方案来防护CSRF攻击,每次登录之后,用户的CSRFTOKEN都不一样,这样就能防护住危险网站的攻击(危险网站不可能随意获取到当前登录用户的CSRFTOKEN)。
<%-- JSP页面,URL后面添加${ctp:csrfSuffix()}做标记 --%>
<form action="${path}/meetingResource.do?method=execAdd${ctp:csrfSuffix()}">
</form>
// javascript在URL最后追加CsrfGuard.getUrlSurffix()
var v3x = new V3X();
function openColInfo(obj) {
var url = _ctxPath + "/coll.do?method=summary&type=" + obj + "&id=123" + CsrfGuard.getUrlSurffix();
v3x.openWindow({
url : url,
workSpace : 'yes',
FullScrean : 'yes',
dialogType : 'open',
closePrevious : "no"
});
}
AJAX请求,增加beforeSend: CsrfGuard.beforeAjaxSend,例如:
$.ajax({
type: "POST",
beforeSend: CsrfGuard.beforeAjaxSend,
url: encodeURI("/seeyon/organization/orgIndexController.do?method=saveRecentData4OrgIndex&rData=" + _value)
});
$('#attchmentForm').ajaxSubmit({
url : genericURL + "?method=updateAttachment&edocSummaryId="+summaryId+"&affairId="+affair_id,
type : 'POST',
beforeSend: CsrfGuard.beforeAjaxSend,
success : function(data) {
}
});
Java后台生成URL时也可以在URL后面追加CSRF标记:
// 返回"&CSRFTOKEN={currentTokenValue}"
com.seeyon.ctp.common.taglibs.functions.Functions.csrfSuffix()
由于CSRF攻击方式复杂、并且一般请求无害,只有涉及到金额、利益相关的行为才需要重视,所以CSRF漏洞监测常用于金融行业。CSRF毕竟要多一层拦截器进行鉴权,性能肯定比不带CSRFTOKEN好,所以我们默认只对核心应用进行CSRF控制。如有特殊URL需要进行控制,需要在下面配置文件中注册。
V5平台的CSRFTOKEN方案基于Owasp封装,WEB-INF/classes/Owasp.CsrfGuard.overlay.properties中配置了需要CSRF控制的URL:
org.owasp.csrfguard.protected.C1 = ajax.do
org.owasp.csrfguard.protected.C2 = content/content.do
org.owasp.csrfguard.protected.C3 = collaboration/collaboration.do?method=saveDraft
# 忽略若干代码 ......
CSRF的拦截器位于:com.seeyon.ctp.common.web.filter.CTPCsrfGuardFilter,过滤器进行了相关防护。
# 十二、服务端请求伪造(SSRF)
SSRF(Server-Side Request Forgery:服务器端请求伪造),形成的原因大都是由于服务端提供了从其他服务器应用获取数据的功能,但又没有对目标地址做严格过滤与限制,导致攻击者可以传入任意的地址来让后端服务器对其发起请求,并返回对该目标地址请求的数据。ssrf是利用存在缺陷的web应用作为代理攻击远程和本地的服务器。
# 攻击方法
服务端提供了从其他服务器应用获取数据的功能且没有对目标地址做过滤与限制,比如从指定URL地址获取网页文本内容,加载指定地址的图片,下载等等。
数据流:攻击者->服务器->目标地址
从服务端获取外部资源的请求,如果该请求参数可控则有可能造成SSRF攻击。比如请求其他服务器的资源测试连接功能,如果没对ip、端口、响应信息做处理则就会出现SSRF攻击,还包括可控的数据库连接测试、加载其他网站图片等等。
通过SSRF漏洞,可能造成的影响包括但不限于:端口信息探测、内网Web应用指纹识别、读取本地文件、攻击内网web应用、攻击运行在内网或本地的应用程序。
# 防御方法
SSRF的防御更多可以从防火墙、网关等层面进行运维级隔离,与代码开发关系相对较小:
1、限制内网IP
2、限制请求高危端口,如:20,21,22,53,80,139,443,445,1433,1521,3306,3389,5432,6379,7001,8080-8090
3、禁用不需要的协议,如:file:///,gopher://,ftpftp://
4、统一回显,过滤详细报错信息