【Step-By-Step】高頻面試題深入解析 / 週刊03

劉小夕發表於2019-06-10

本週面試題一覽:

  • 什麼是XSS攻擊,XSS 攻擊可以分為哪幾類?我們如何防範XSS攻擊?
  • 如何隱藏頁面中的某個元素?
  • 瀏覽器事件代理機制的原理是什麼?
  • setTimeout 倒數計時為什麼會出現誤差?

更多優質文章可戳: github.com/YvetteLau/B…

11. 什麼是XSS攻擊,XSS攻擊可以分為哪幾類?我們如何防範XSS攻擊?

1. XSS攻擊

XSS(Cross-Site Scripting,跨站指令碼攻擊)是一種程式碼注入攻擊。攻擊者在目標網站上注入惡意程式碼,當被攻擊者登陸網站時就會執行這些惡意程式碼,這些指令碼可以讀取 cookie,session tokens,或者其它敏感的網站資訊,對使用者進行釣魚欺詐,甚至發起蠕蟲攻擊等。

XSS 的本質是:惡意程式碼未經過濾,與網站正常的程式碼混在一起;瀏覽器無法分辨哪些指令碼是可信的,導致惡意指令碼被執行。由於直接在使用者的終端執行,惡意程式碼能夠直接獲取使用者的資訊,利用這些資訊冒充使用者向網站發起攻擊者定義的請求。

XSS分類

根據攻擊的來源,XSS攻擊可以分為儲存型(永續性)、反射型(非持久型)和DOM型三種。下面我們來詳細瞭解一下這三種XSS攻擊:

1.1 反射型XSS

當使用者點選一個惡意連結,或者提交一個表單,或者進入一個惡意網站時,注入指令碼進入被攻擊者的網站。Web伺服器將注入指令碼,比如一個錯誤資訊,搜尋結果等,未進行過濾直接返回到使用者的瀏覽器上。

反射型 XSS 的攻擊步驟:

  1. 攻擊者構造出特殊的 URL,其中包含惡意程式碼。
  2. 使用者開啟帶有惡意程式碼的 URL 時,網站服務端將惡意程式碼從 URL 中取出,拼接在 HTML 中返回給瀏覽器。
  3. 使用者瀏覽器接收到響應後解析執行,混在其中的惡意程式碼也被執行。
  4. 惡意程式碼竊取使用者資料併傳送到攻擊者的網站,或者冒充使用者的行為,呼叫目標網站介面執行攻擊者指定的操作。

反射型 XSS 漏洞常見於通過 URL 傳遞引數的功能,如網站搜尋、跳轉等。由於需要使用者主動開啟惡意的 URL 才能生效,攻擊者往往會結合多種手段誘導使用者點選。

POST 的內容也可以觸發反射型 XSS,只不過其觸發條件比較苛刻(需要構造表單提交頁面,並引導使用者點選),所以非常少見。

如果不希望被前端拿到cookie,後端可以設定 httpOnly (不過這不是 XSS攻擊 的解決方案,只能降低受損範圍)

如何防範反射型XSS攻擊

對字串進行編碼。

對url的查詢引數進行轉義後再輸出到頁面。

app.get('/welcome', function(req, res) {
    //對查詢引數進行編碼,避免反射型 XSS攻擊
    res.send(`${encodeURIComponent(req.query.type)}`); 
});
複製程式碼

1.2 DOM 型 XSS

DOM 型 XSS 攻擊,實際上就是前端 JavaScript 程式碼不夠嚴謹,把不可信的內容插入到了頁面。在使用 .innerHTML.outerHTML.appendChilddocument.write()等API時要特別小心,不要把不可信的資料作為 HTML 插到頁面上,儘量使用 .innerText.textContent.setAttribute() 等。

DOM 型 XSS 的攻擊步驟:

  1. 攻擊者構造出特殊資料,其中包含惡意程式碼。
  2. 使用者瀏覽器執行了惡意程式碼。
  3. 惡意程式碼竊取使用者資料併傳送到攻擊者的網站,或者冒充使用者的行為,呼叫目標網站介面執行攻擊者指定的操作。

如何防範 DOM 型 XSS 攻擊

防範 DOM 型 XSS 攻擊的核心就是對輸入內容進行轉義(DOM 中的內聯事件監聽器和連結跳轉都能把字串作為程式碼執行,需要對其內容進行檢查)。

1.對於url連結(例如圖片的src屬性),那麼直接使用 encodeURIComponent 來轉義。

2.非url,我們可以這樣進行編碼:

function encodeHtml(str) {
    return str.replace(/"/g, '"')
            .replace(/'/g, ''')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;');
}
複製程式碼

DOM 型 XSS 攻擊中,取出和執行惡意程式碼由瀏覽器端完成,屬於前端 JavaScript 自身的安全漏洞。

1.3 儲存型XSS

惡意指令碼永久儲存在目標伺服器上。當瀏覽器請求資料時,指令碼從伺服器傳回並執行,影響範圍比反射型和DOM型XSS更大。儲存型XSS攻擊的原因仍然是沒有做好資料過濾:前端提交資料至服務端時,沒有做好過濾;服務端在接受到資料時,在儲存之前,沒有做過濾;前端從服務端請求到資料,沒有過濾輸出。

儲存型 XSS 的攻擊步驟:

  1. 攻擊者將惡意程式碼提交到目標網站的資料庫中。
  2. 使用者開啟目標網站時,網站服務端將惡意程式碼從資料庫取出,拼接在 HTML 中返回給瀏覽器。
  3. 使用者瀏覽器接收到響應後解析執行,混在其中的惡意程式碼也被執行。
  4. 惡意程式碼竊取使用者資料併傳送到攻擊者的網站,或者冒充使用者的行為,呼叫目標網站介面執行攻擊者指定的操作。

這種攻擊常見於帶有使用者儲存資料的網站功能,如論壇發帖、商品評論、使用者私信等。

如何防範儲存型XSS攻擊:

  1. 前端資料傳遞給伺服器之前,先轉義/過濾(防範不了抓包修改資料的情況)
  2. 伺服器接收到資料,在儲存到資料庫之前,進行轉義/過濾
  3. 前端接收到伺服器傳遞過來的資料,在展示到頁面前,先進行轉義/過濾

除了謹慎的轉義,我們還需要其他一些手段來防範XSS攻擊:

1.Content Security Policy

在服務端使用 HTTP的 Content-Security-Policy 頭部來指定策略,或者在前端設定 meta 標籤。

例如下面的配置只允許載入同域下的資源:

Content-Security-Policy: default-src 'self'
複製程式碼
<meta http-equiv="Content-Security-Policy" content="form-action 'self';">
複製程式碼

前端和服務端設定 CSP 的效果相同,但是meta無法使用report

嚴格的 CSP 在 XSS 的防範中可以起到以下的作用:

  1. 禁止載入外域程式碼,防止複雜的攻擊邏輯。
  2. 禁止外域提交,網站被攻擊後,使用者的資料不會洩露到外域。
  3. 禁止內聯指令碼執行(規則較嚴格,目前發現 GitHub 使用)。
  4. 禁止未授權的指令碼執行(新特性,Google Map 移動版在使用)。
  5. 合理使用上報可以及時發現 XSS,利於儘快修復問題。

2.輸入內容長度控制

對於不受信任的輸入,都應該限定一個合理的長度。雖然無法完全防止 XSS 發生,但可以增加 XSS 攻擊的難度。

3.輸入內容限制

對於部分輸入,可以限定不能包含特殊字元或者僅能輸入數字等。

4.其他安全措施

  • HTTP-only Cookie: 禁止 JavaScript 讀取某些敏感 Cookie,攻擊者完成 XSS 注入後也無法竊取此 Cookie。
  • 驗證碼:防止指令碼冒充使用者提交危險操作。

點選檢視更多

12.如何隱藏頁面中的某個元素?

隱藏型別

螢幕並不是唯一的輸出機制,比如說螢幕上看不見的元素(隱藏的元素),其中一些依然能夠被讀屏軟體閱讀出來(因為讀屏軟體依賴於可訪問性樹來闡述)。為了消除它們之間的歧義,我們將其歸為三大類:

  • 完全隱藏:元素從渲染樹中消失,不佔據空間。
  • 視覺上的隱藏:螢幕中不可見,佔據空間。
  • 語義上的隱藏:讀屏軟體不可讀,但正常佔據空。

完全隱藏

1.display 屬性(不佔據空間)
display: none;
複製程式碼
2.hidden 屬性 (不佔據空間)

HTML5 新增屬性,相當於 display: none

<div hidden>
</div>
複製程式碼

視覺上的隱藏

1.利用 position 和 盒模型 將元素移出可視區範圍
  1. 設定 posoitionabsolutefixed,通過設定 topleft 等值,將其移出可視區域。(可視區域不佔位)
position:absolute;
left: -99999px;
複製程式碼
  1. 設定 positionrelative,通過設定 topleft 等值,將其移出可視區域。(可視區域佔位)
position: relative;
left: -99999px;
複製程式碼

如希望其在可視區域不佔位置,需同時設定 height: 0;

  1. 設定 margin 值,將其移出可視區域範圍(可視區域佔位)。
margin-left: -99999px;
複製程式碼

如果希望其在可視區域不佔位,需同時設定 height: 0;

2.利用 transfrom
  1. 縮放(佔據空間)
transform: scale(0);
複製程式碼

如果希望不佔據空間,需同時設定 height: 0

  1. 移動 translateX, translateY(佔據空間)
transform: translateX(-99999px);
複製程式碼

如果希望不佔據空間,需同時設定 height: 0

  1. 旋轉 rotate (佔據空間)
transform: rotateY(90deg);
複製程式碼
3.設定其大小為0

寬高為0,字型大小為0:

height: 0;
width: 0;
font-size: 0;
複製程式碼

寬高為0,超出隱藏:

height: 0;
width: 0;
overflow: hidden;
複製程式碼
4.設定透明度為0 (佔據空間)
opacity: 0;
複製程式碼
5.visibility屬性 (佔據空間)
visibility: hidden
複製程式碼
6.層級覆蓋,z-index 屬性 (佔據空間)
position: relative;
z-index: -999;
複製程式碼

再設定一個層級較高的元素覆蓋在此元素上。

7.clip-path 裁剪 (佔據空間)
clip-path: polygon(0 0, 0 0, 0 0, 0 0);
複製程式碼

語義上的隱藏

aria-hidden 屬性 (佔據空間)

讀屏軟體不可讀,佔據空間,可見。

<div aria-hidden="true">
</div>
複製程式碼

11. 使用JS將元素從頁面中移除

點選檢視更多

13.瀏覽器事件代理機制的原理是什麼?

事件流

在說瀏覽器事件代理機制原理之前,我們首先了解一下事件流的概念,早期瀏覽器,IE採用的是事件捕獲事件流,而Netscape採用的則是事件冒泡。"DOM2級事件"把事件流分為三個階段,捕獲階段、目標階段、冒泡階段。現代瀏覽器也都遵循此規範。

【Step-By-Step】高頻面試題深入解析 / 週刊03

事件代理機制的原理

事件代理又稱為事件委託,在祖先級 DOM 元素繫結一個事件,當觸發子孫級DOM元素的事件時,利用事件冒泡的原理來觸發繫結在祖先級 DOM 的事件。因為事件會從目標元素一層層冒泡至 document 物件。

為什麼要事件代理?

  1. 新增到頁面上的事件數量會影響頁面的執行效能,如果新增的事件過多,會導致網頁的效能下降。採用事件代理的方式,可以大大減少註冊事件的個數。

  2. 事件代理的當時,某個子孫元素是動態增加的,不需要再次對其進行事件繫結。

  3. 不用擔心某個註冊了事件的DOM元素被移除後,可能無法回收其事件處理程式,我們只要把事件處理程式委託給更高層級的元素,就可以避免此問題。

  4. 允許給一個事件註冊多個監聽。

  5. 提供了一種更精細的手段控制 listener 的觸發階段(可以選擇捕獲或者是冒泡)。

  6. 對任何 DOM 元素都是有效的,而不僅僅是對 HTML 元素有效。

addEventListener

addEventListener 接受3個引數,分別是要處理的事件名、實現了 EventListener 介面的物件或者是一個函式、一個物件/一個布林值。

target.addEventListener(type, listener[, options]);
target.addEventListener(type, listener[, useCapture]);
複製程式碼

options(物件) | 可選

  • capture: Boolean。true 表示在捕獲階段觸發,false表示在冒泡階段觸發。預設是 false。

  • once:Boolean。true 表示listener 在新增之後最多隻呼叫一次,listener 會在其被呼叫之後自動移除。預設是 false。

  • passive: Boolean。true 表示 listener 永遠不會呼叫 preventDefault()。如果 listener 仍然呼叫了這個函式,客戶端將會忽略它並丟擲一個控制檯警告。預設是 false。

useCapture(Boolean) | 可選

useCapture 預設為 false。表示冒泡階段呼叫事件處理程式,若設定為 true,表示在捕獲階段呼叫事件處理程式。

如將頁面中的所有click事件都代理到document上:

document.addEventListener('click', function (e) {
    console.log(e.target);
    /**
    * 捕獲階段呼叫呼叫事件處理程式,eventPhase是 1; 
    * 處於目標,eventPhase是2 
    * 冒泡階段呼叫事件處理程式,eventPhase是 3;
    */ 
    console.log(e.eventPhase);
    
}, false);
複製程式碼

addEventListener 相對應的是 removeEventListener,用於移除事件監聽。

點選檢視更多

14. setTimeout 倒數計時為什麼會出現誤差?

setTimeout 只能保證延時或間隔不小於設定的時間。因為它實際上只是將回撥新增到了巨集任務佇列中,但是如果主執行緒上有任務還沒有執行完成,它必須要等待。

如果你對前面這句話不是非常理解,那麼有必要了解一下 JS的執行機制。

JS的執行機制

(1)所有同步任務都在主執行緒上執行,形成一個執行棧(execution context stack)。

(2)主執行緒之外,還存在"任務佇列"(task queue)。

(3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務佇列",看看裡面有哪些事件。那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行。

(4)主執行緒不斷重複上面的第三步。

setTimeout(()=>{callback();}, 1000) ,即表示在1s之後將 callback 放到巨集任務佇列中,當1s的時間到達時,如果主執行緒上有其它任務在執行,那麼 callback 就必須要等待,另外 callback 的執行也需要時間,因此 setTimeout 的時間間隔是有誤差的,它只能保證延時不小於設定的時間。

如何減少 setTimeout 的誤差

我們只能減少執行多次的 setTimeout 的誤差,例如倒數計時功能。

倒數計時的時間通常都是從服務端獲取的。造成誤差的原因:

1.沒有考慮誤差時間(函式執行的時間/其它程式碼的阻塞)

2.沒有考慮瀏覽器的“休眠”

完全消除 setTimeout的誤差是不可能的,但是我們減少 setTimeout 的誤差。通過對下一次任務的呼叫時間進行修正,來減少誤差。

let count = 0;
let countdown = 5000; //伺服器返回的倒數計時時間
let interval = 1000;
let startTime = new Date().getTime();
let timer = setTimeout(countDownStart, interval); //首次執行
//定時器測試
function countDownStart() {
    count++;
    const offset = new Date().getTime() - (startTime + count * 1000);
    const nextInterval = interval - offset; //修正後的延時時間
    if (nextInterval < 0) {
        nextInterval = 0;
    }
    countdown -= interval;
    console.log("誤差:" + offset + "ms,下一次執行:" + nextInterval + "ms後,離活動開始還有:" + countdown + "ms");
    if (countdown <= 0) {
        clearTimeout(timer);
    } else {
        timer = setTimeout(countDownStart, nextInterval);
    }
}

複製程式碼

如果當前頁面是不可見的,那麼倒數計時會出現大於100ms的誤差時間。因此在頁面顯示時,應該重新從服務端獲取剩餘時間進行倒數計時。當然,為了更好的效能,當倒數計時不可見(Tab頁切換/倒數計時內容不在可視區時),可以選擇停止倒數計時。

為此,我們可以監聽 visibityChange 事件進行處理。

點選檢視更多

參考文章:

[1] MDN addEventListener

[2] www.ecma-international.org/ecma-262/6.…

[3] www.xuanfengge.com/js-realizes…

謝謝各位小夥伴願意花費寶貴的時間閱讀本文,如果本文給了您一點幫助或者是啟發,請不要吝嗇你的贊和Star,您的肯定是我前進的最大動力。github.com/YvetteLau/B…

關注公眾號,加入技術交流群

【Step-By-Step】高頻面試題深入解析 / 週刊03

相關文章