一.背景
本次测试使用到的主要工具包含:eclipse、谷歌浏览器、Windows11家庭版、ApiPost。
(一)发生的问题
公司安全兄弟提示我们一个应用存在跨域攻击的漏洞,需要我们修复。扫描情况及整改建议如下:
昨天晚上扫描了。有2个高危,1个中危,
高危1:CORS (Cross-Origin Resource Sharing) origin validation failure
具体说明:我们使用awvs扫描,模拟了来自不同源请求,然后目标服务器允许了这些跨域请求,没有正确验证请求的来源,这可能导致web应用受到跨站请求伪造等攻击
解决办法:
1.审查和加强CORS策略:
仔细检查服务器的CORS配置。
只允许必要的源进行跨域请求。
使用具体的域名,避免使用通配符(*)。
2.正确配置 'Access-Control-Allow-Origin' 头:
明确列出允许的源,而不是使用 '*'。
例如:Access-Control-Allow-Origin: https://trusted-site.com
3.实现动态CORS策略:
根据请求的 Origin 头动态设置 'Access-Control-Allow-Origin'。
这样可以更精确地控制允许的源。
4.使用预检请求(Preflight Requests):
对于非简单请求,确保正确处理 OPTIONS 请求。
5.设置 'Access-Control-Allow-Methods' 和 'Access-Control-Allow-Headers'。
设置 'Access-Control-Allow-Credentials':
如果需要发送cookies或授权头,将此设为 'true'。
注意:这时 'Access-Control-Allow-Origin' 不能设为 '*'。
6.实现额外的安全措施:
使用CSRF令牌来防止CSRF攻击。
实施内容安全策略(CSP)来进一步加强安全性。
Node.js代码解决方法:
const express = require('express');
const cors = require('cors');
const app = express();const allowedOrigins = ['https://trusted-site1.com', 'https://trusted-site2.com'];app.use(cors({origin: function (origin, callback) {if (!origin) return callback(null, true);if (allowedOrigins.indexOf(origin) === -1) {const msg = 'The CORS policy for this site does not allow access from the specified Origin.';return callback(new Error(msg), false);}return callback(null, true);},methods: ['GET', 'POST', 'PUT', 'DELETE'],allowedHeaders: ['Content-Type', 'Authorization'],credentials: true
}));// 其余的应用逻辑...app.listen(3000, () => console.log('Server running on port 3000'));
(二)被扫描应用的情况说明
公司给一些固定资产贴了标签,标签上面有几个关键的文件信息和一个二维码。二维码就是一个二级域名+设备ID的链接,徒弟做了一个响应扫码的应用,就是拿着设备ID去查询最新的详细信息返回来,让扫描的人可以看到详细的信息。应用部署在内网,通过代理映射出去了。不需要登录,微信等就可以直接扫描。
应用分为前端和后端,分开部署。前端是一个类似卡片的界面,后端打开了允许所有地址跨域访问(Access-Control-Allow-Origin:*)。又因为这个界面不要求登录,又是互联网可以访问的,所以出现了跨域攻击风险。
二.解决思路
安全组的兄弟的反馈里面已经给了解决思路。
1.要么不允许跨域访问,让前后端部署在一起,可以解决
2.要么允许跨域,但是需要指定允许跨域的地址
三.需要给徒弟讲明白的几个问题
(一)什么是跨域访问?
先看看AI的回答:
跨域访问(Cross-Origin Resource Sharing, CORS)是一个网络安全功能,它允许或拒绝网页的某些资源(如字体、JavaScript、CSS等)从与发起请求的网页不同的源(域名、协议或端口)进行加载。这种机制通过服务器发送的响应头来告诉浏览器哪些源有权访问该资源。
AI的回答给我的几个启示:
1.你浏览器访问的地址就是源,网页默认或者用户操作发起的其他请求地址与源不相同就是跨域了。
2.是否相同的判断包含域名、协议、端口三个,三个有一点不同就是跨域了。
3.是否允许跨域是资源方通过返回请求头中设置的信息来告诉请求方的。
4.是不是跨域了,最终还是浏览器来决定的。
(二)模拟跨域访问
既然跨域是考验的协议、域名、端口,那本地准备2个应用,第一个是cors-demo1,访问端口是http://127.0.0.1:8081,默认访问index.html。第二个应用是cors-demo2,访问端口是http://127.0.0.1:8082。在应用1的index.html里面有个按钮,点击就访问应用2的/hello2,这样就是跨域访问了。
1.工程cors-demo1
pom.xml内容:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.scantt</groupId><artifactId>cors-demo1</artifactId><version>0.2.6-SNAPSHOT</version><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.7.0</version></dependency></dependencies>
</project>
CorsDemo1Application.java
package com.scantt.demo1;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class CorsDemo1Application {public static void main(String[] args) {SpringApplication.run(CorsDemo1Application.class, args);}}
application.properties
server.port=8081
src/main/resources/static/index.html
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>Insert title here</title><script type="text/javascript" src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
</head>
<body><button onclick="click1()">跨域失败</button><div id="result"></div>
</body>
<script type="text/javascript">function click1() {$.ajax({url: 'http://localhost:8082/hello2',method: 'get',success: function (res) {console.log(res);$('#result').append('<p>' + res + '</p>');}});}
</script>
</html>
2.工程cors-demo2
pom.xml内容:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.scantt</groupId><artifactId>cors-demo2</artifactId><version>0.2.6-SNAPSHOT</version><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.7.0</version></dependency></dependencies>
</project>
CorsDemo2Application.java
package com.scantt.demo1;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class CorsDemo2Application {public static void main(String[] args) {SpringApplication.run(CorsDemo2Application.class, args);}}
application.properties
server.port=8082
CorsDemo2Controller.java
package com.scantt.demo2.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class CorsDemo2Controller {@GetMapping("/hello2")public String hello2() {System.out.println("请求了2");return "hello2";}
}
3.操作模拟跨域
F12打开调试后点击“跨域失败”按钮如下:
通过提示,我们看到出现了跨域访问失败的显示。
应用2后台打印了请问访问,说明虽然浏览器提示跨域出错,但是后端应用仍然被调用到了的。
从请求返回来看,并没有返回期望的请求头“Access-Control-Allow-Origin”,所以浏览器,默认是认为不允许跨域访问的。
4.测试工具直接访问应用2
使用测试工具直接访问http://127.0.0.1:8082/hello2是可以直接请求到应用2的,并没有跨域一说。
(三)模拟限制指定跨域访问
1.工程cors-demo3
pom.xml内容:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.scantt</groupId><artifactId>cors-demo3</artifactId><version>0.2.6-SNAPSHOT</version><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.7.0</version></dependency></dependencies>
</project>
CorsDemo3Application.java
package com.scantt.demo3;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CorsDemo3Application {public static void main(String[] args) {SpringApplication.run(CorsDemo3Application.class, args);}
}
CorsDemo3Controller.java
package com.scantt.demo3.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class CorsDemo3Controller {@GetMapping("/hello3")public String hello3() {System.out.println("请求了3");return "hello3";}
}
CorsConfig.java
package com.scantt.demo3.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig {@Beanpublic WebMvcConfigurer corsConfigurer() {System.out.println("已经启用了");return new WebMvcConfigurer() {@Override// 重写父类提供的跨域请求处理的接口public void addCorsMappings(CorsRegistry registry) {// 添加映射路径CorsRegistration cr = registry.addMapping("/**");// 是否发送Cookie信息cr.allowCredentials(true)// 放行哪些原始域(请求方式).allowedMethods("GET", "POST", "PUT", "DELETE")// 放行哪些原始域(头部信息).allowedHeaders("*");// 限制跨域地址为http://127.0.0.1:8081cr.allowedOrigins("http://127.0.0.1:8081");// 允许任何地址跨域访问
// cr.allowedOriginPatterns("*");}};}
}
application.properties
server.port=8083
2.操作模拟允许跨域
打开浏览器F12后,点击“允许指定服务器跨域”按钮(当然自己要去修改应用1中的index.html哈,加一个按钮和请求函数,改一点,应该会吧):
访问后,界面有正常的反应,多了个hello3:
我们看看浏览器调试里面,请求是正常的:
查看请求的Headers,可以看到Access-Control-Allow-Origin,告诉了浏览器允许跨域访问的地址,就是你浏览器访问时访问的网站地址哦!
(四)有没有可能再安全的加强一点跨域限制?
1.对请求头的探索
从前面的验证知道,即便浏览器拒绝了跨域访问,实际上后端服务还是被访问到了的。我们能否从请求中就知道本来的域是什么,我直接就不让来自白名单以外的请求访问我后端服务?
浏览器的Headers中,request header里面有个参数叫Origin,就是原始访问域的意思。如刚刚的请求:
那我后端可以拿到这个header,判断其源是否在白名单中,如果没有就拒绝访问,这样后端应用就不会被跨域请求调用到了。当然,我找了很多不同的网站请求里面,发现并不是每个请求都会加上Origin,但是发现Referer却几乎都有,他们有啥区别呢?AI回答如下:
HTTP请求头中的Origin
和Referer
字段都用于表示请求的来源,但它们在用途、格式和安全性方面存在一些区别。
用途
- Referer:主要用于记录用户是从哪个页面链接到当前页面的,有助于网站分析、统计、日志记录、广告跟踪等。它指明了用户是从哪个页面跳转或提交请求的。
- Origin:主要用于跨站请求的安全性检查。在跨域请求时,浏览器会检查目标服务器是否允许来自特定
Origin
的请求,如果不允许,浏览器会阻止此类请求。它用于表示一个URI的起源,包括协议、主机和端口。
格式
- Referer:包含了源页面的完整URL,包括协议、主机、端口(如果有的话)、路径、查询参数等。
- Origin:只包含协议、主机和端口,不包含路径、查询参数等。例如,
Origin: https://example.com
。
安全性
- Referer:虽然Referer也可以在某些场景下用于防范CSRF(跨站请求伪造)攻击,但其值可以由用户和服务端控制,因此不是一种可靠的安全控制手段。用户可以通过修改浏览器设置或使用插件等方式来篡改或隐藏Referer信息。
- Origin:由浏览器自动设置,通常用于跨站请求的安全性检查。它的值不能手动修改,因此在防范CSRF攻击时更为可靠。
存在场景
- Referer:存在于所有类型的HTTP请求中,无论是GET请求还是POST请求。
- Origin:主要用于跨站请求,如通过
XMLHttpRequest
或fetch
等API发起的跨域请求。在普通的页面跳转或链接点击中,可能不会发送Origin
字段。
总结
Referer
和Origin
都是HTTP请求头中用于表示请求来源的字段,但它们在用途、格式、安全性和存在场景上有所不同。Referer
主要用于追踪用户来源和统计分析,而Origin
则更侧重于跨站请求的安全性检查。在开发过程中,应根据实际需求选择合适的字段进行使用。
从AI的回答来看,这2个参数都可以用来我们做请求拦截的判断,但都不是浏览器必须传递的。我们可以添加请求拦截器,预设允许跨域白名单,根据请求header中的Origin及Referer判断是否在允许的白名单中,如果2个header都没有值,就默认返回允许跨域的值。
2.工程cors-demo4
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.scantt</groupId><artifactId>cors-demo4</artifactId><version>0.2.6-SNAPSHOT</version><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.7.0</version></dependency></dependencies>
</project>
CorsDemo4Application.java
package com.scantt.demo4;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CorsDemo4Application {public static void main(String[] args) {SpringApplication.run(CorsDemo4Application.class, args);}
}
CorsDemo4Controller.java
package com.scantt.demo4.controller;import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class CorsDemo4Controller {@GetMapping("/hello4")public String hello2() {System.out.println("调用了demo4!");return "hello4";}
}
CorsInterceptor.java
package com.scantt.demo4.config;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class CorsInterceptor implements HandlerInterceptor{ @Value(value = "${sys.config.corsAllows:http://127.0.0.1:8081}")private String corsAllow;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {String requestHeaderOrigin =request.getHeader("Origin");String requestHeaderReferer =request.getHeader("Referer");if(StringUtils.hasLength(requestHeaderOrigin)) {if(requestHeaderOrigin.endsWith("/")) {requestHeaderOrigin=requestHeaderOrigin.substring(0,requestHeaderOrigin.length()-1);}if(!corsAllow.contains(requestHeaderOrigin)) {System.out.println("不让调用!");//不让调用response.setStatus(HttpServletResponse.SC_OK);return false;}else {System.out.println("requestHeaderOrigin在允许的列表中!");response.setHeader("Access-Control-Allow-Origin", requestHeaderOrigin);}}else {if(StringUtils.hasLength(requestHeaderReferer)) {if(requestHeaderReferer.endsWith("/")) {requestHeaderReferer=requestHeaderReferer.substring(0,requestHeaderReferer.length()-1);}if(!corsAllow.contains(requestHeaderReferer)) {System.out.println("不让调用!");//不让调用response.setStatus(HttpServletResponse.SC_OK);return false;}else {System.out.println("requestHeaderReferer在允许的列表中!");response.setHeader("Access-Control-Allow-Origin", requestHeaderReferer);}}} response.setHeader("Access-Control-Allow-Credentials", "true");response.setHeader("Access-Control-Allow-Methods", "*");response.setHeader("Access-Control-Max-Age", "86400");response.setHeader("Access-Control-Allow-Headers", "*");if(HttpMethod.OPTIONS.toString().equals(request.getMethod())) {response.setStatus(HttpServletResponse.SC_OK);return false;}return true;}
}
CorsInterceptorConfig.java
package com.scantt.demo4.config;import javax.annotation.Resource;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsInterceptorConfig implements WebMvcConfigurer {@Resourceprivate CorsInterceptor corsInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(corsInterceptor).addPathPatterns("/**");}
}
application.properties
server.port=8084
3.操作验证更安全的跨域限制
打开F12,点击“更安全的限制跨域”按钮(当然自己要去修改应用1中的index.html哈,加一个按钮和请求函数,改一点,应该会吧):
访问结果肯定跟预期一致的:
从请求头来分析看,跟预期也是一致的:
那我们把允许跨域的地址随便修改为http://127.0.0.1:8000试试?修改CorsInterceptor.java,把现在允许跨域的端口8081修改为8000,我们试试结果?(修改java,需要重启应用4)
再次点击“更安全的跨域限制”界面没有效果,浏览器调试出现了跨域提示了:
后台打印内容显示,并没有真实执行我们的controller代码,相比之前更安全了:
而且,我们把允许跨域地址用springboot的@Value注解的这个做成了参数配置,更便于其他应用来复用这个设置功能。
(五)禁止跨域访问或者允许跨域地址以外的访问,http测试工具不提示跨域访问拒绝,但浏览器却提示跨域拒绝的原因是什么?
前面测试过了,是否跨域虽然在服务端有设置,但是判断是访问端来执行的。浏览器有安全机制,所以有提示。测试工具没有这个机制,所以并不会报错。
(六)HTTP的一次请求里面到底有些什么东西?谁定义的?有版本区分吗?怎么查看版本?
1.HTTP协议的类型及制定的组织
前面的测试,也把跟跨域有关的Header说明白了。至于HTTP协议嘛,问的AI:
HTTP协议,全称HyperText Transfer Protocol,即超文本传输协议,是互联网上应用最为广泛的一种网络协议。它规定了浏览器和web服务器之间通信的规则,是万维网(World Wide Web)的数据通信的基础。以下是关于HTTP协议的一些详细解答:
HTTP协议的类型
HTTP协议随着技术的发展不断演进,目前主要包括以下几个版本:
- HTTP/0.9:这是HTTP的最初版本,非常简单,仅支持GET方法,不支持请求头(Headers)和响应头(Headers),服务器只能返回HTML格式的文档。
- HTTP/1.0:该版本增加了请求头(Headers)和响应头(Headers),引入了POST和HEAD方法,并且支持长连接(通过Connection: keep-alive头)。
- HTTP/1.1:在HTTP/1.0的基础上,HTTP/1.1引入了更多的缓存控制策略、传输编码(如分块传输编码)和更多的请求方法(如PUT、DELETE、OPTIONS等),并且增加了对持久连接的支持。
- HTTP/2:HTTP/2对HTTP/1.1进行了大幅度改造,引入了头部压缩、二进制格式、服务器推送等功能,大大提升了HTTP的性能。
- HTTP/3:HTTP/3基于QUIC协议,是一个实验性的版本,旨在解决HTTP/2的一些问题和进一步提升性能。
编写组织
HTTP协议是由多个组织共同制定和维护的,其中最主要的两个组织是:
- 万维网联盟(W3C):万维网联盟是负责互联网技术标准制定和维护的重要组织之一,虽然HTTP协议的具体规范并不完全由W3C制定,但W3C在推动Web技术发展方面发挥了重要作用。
- 互联网工程任务组(IETF):IETF是负责互联网技术标准制定和维护的主要组织之一,HTTP协议的具体规范(如RFC 2616定义了HTTP/1.1)就是由IETF的工作组制定和发布的。
需要注意的是,虽然HTTP协议的具体规范是由这些组织制定的,但在实际应用中,HTTP协议的实现和遵守是由各种浏览器、服务器和其他网络设备来完成的。这些设备需要按照HTTP协议的规范来构建请求和响应,以确保通信的顺利进行。
总结
HTTP协议是互联网上应用最为广泛的一种网络协议,它规定了浏览器和web服务器之间通信的规则。HTTP协议随着技术的发展不断演进,目前主要包括HTTP/0.9、HTTP/1.0、HTTP/1.1、HTTP/2和HTTP/3等版本。HTTP协议是由多个组织共同制定和维护的,其中最主要的两个组织是万维网联盟(W3C)和互联网工程任务组(IETF)。
2.查看某个请求的HTTP协议版本
C:\Users\Dell>curl -I http://127.0.0.1:8081
HTTP/1.1 200
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Last-Modified: Tue, 24 Sep 2024 03:48:50 GMT
Accept-Ranges: bytes
Content-Type: text/html
Content-Language: zh-CN
Content-Length: 1063
Date: Tue, 24 Sep 2024 04:10:50 GMTC:\Users\Dell>curl -I http://127.0.0.1:8082/hello2
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 6
Date: Tue, 24 Sep 2024 04:10:52 GMTC:\Users\Dell>
我的是HTTP/1.1版本。
(七)之前的应用用nginx代理时,为何不提示跨域访问的问题?
一方面是我之前的应用nginx、前端、后端都在同一个服务器,我后端还设置的允许任何地址的跨域访问。后面有时间再单独看看nginx跟跨域的相关配置吧!!!
(八)CSRF是怎么攻击的?
看看AI的回答:
1. 用户登录受信任网站
- 用户(如用户C)打开浏览器,访问一个受信任的网站(如网站A),并输入用户名和密码进行登录。
- 网站A验证用户信息后,生成Cookie信息并返回给浏览器。此时,用户成功登录网站A,并可以正常发送请求到网站A。
2. 用户访问恶意网站
- 在用户未退出网站A之前,在同一浏览器中,用户打开一个新的标签页或窗口访问一个恶意网站(如网站B)。
- 网站B可能是攻击者精心构建的,用于诱骗用户执行某些操作或仅仅是为了在用户不知情的情况下发起CSRF攻击。
3. 恶意网站发送伪造请求
- 网站B接收到用户请求后,可能会返回一些攻击性代码,并发出一个请求要求访问第三方站点(即之前用户已登录的网站A)。
- 这些攻击性代码可能通过隐藏的表单、iframe、JavaScript自动提交表单等方式实现。
4. 浏览器携带Cookie发送请求
- 浏览器在接收到网站B的攻击性代码后,会根据网站B的请求,在用户不知情的情况下携带之前从网站A获取的Cookie信息,向网站A发出请求。
- 由于HTTP请求是无状态的,并且浏览器之前已经通过身份验证并获得了网站A的Cookie,因此网站A无法区分这个请求是来自用户还是来自攻击者。
5. 网站A处理恶意请求
- 网站A接收到请求后,会根据用户C的Cookie信息以C的权限处理该请求。由于请求中可能包含了恶意的内容(如转账、修改账户信息等),因此这些操作会被执行,导致用户C的权益受到损害。