一次意外的百度二面

語言: CN / TW / HK

theme: qklhk-chocolate

一次令人窒息的百度面試之後,意外的得到了二面的機會~~~

自我介紹

首先上來是一段自我介紹,主要圍繞前端的開發經驗進行了自我介紹。

看簡歷

之後就是看簡歷,面試官主要圍繞專案進行詢問。

技術難點

這個主要是根據個人經歷詢問,在此就不贅述了。但是針對技術難點,面試官往往會進行深挖。

工作過程中有沒有遇到一些問題並且提出解決方案

大檔案上傳切割後如何上傳的

可以看一下這篇文章,詳細記錄了針對大檔案上傳的一些追問。

講一下xss和scrf

XSS 攻擊

Cross-Site Scripting(跨站指令碼攻擊)簡稱 XSS,是一種程式碼注入攻擊。攻擊者通過在目標網站上注入惡意指令碼,使之在使用者的瀏覽器上執行。利用這些惡意指令碼,攻擊者可獲取使用者的敏感資訊如 Cookie、SessionID 等,進而危害資料安全。

XSS 的本質是:惡意程式碼未經過濾,與網站正常的程式碼混在一起;瀏覽器無法分辨哪些指令碼是可信的,導致惡意指令碼被執行。

而由於直接在使用者的終端執行,惡意程式碼能夠直接獲取使用者的資訊,或者利用這些資訊冒充使用者向網站發起攻擊者定義的請求。

在部分情況下,由於輸入的限制,注入的惡意指令碼比較短。但可以通過引入外部的指令碼,並由瀏覽器執行,來完成比較複雜的攻擊策略。

XSS 攻擊可分為儲存型、反射型和 DOM 型三種:

儲存型 XSS

儲存型 XSS 的攻擊步驟:

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

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

反射型 XSS

反射型 XSS 的攻擊步驟:

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

反射型 XSS 跟儲存型 XSS 的區別是:儲存型 XSS 的惡意程式碼存在資料庫裡,反射型 XSS 的惡意程式碼存在 URL 裡。

反射型 XSS 漏洞常見於通過 URL 傳遞引數的功能,如網站搜尋、跳轉等。

由於需要使用者主動開啟惡意的 URL 才能生效,攻擊者往往會結合多種手段誘導使用者點選。

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

DOM 型 XSS

DOM 型 XSS 的攻擊步驟:

  1. 攻擊者構造出特殊的 URL,其中包含惡意程式碼。
  2. 使用者開啟帶有惡意程式碼的 URL。
  3. 使用者瀏覽器接收到響應後解析執行,前端 JavaScript 取出 URL 中的惡意程式碼並執行。
  4. 惡意程式碼竊取使用者資料併發送到攻擊者的網站,或者冒充使用者的行為,呼叫目標網站介面執行攻擊者指定的操作。

DOM 型 XSS 跟前兩種 XSS 的區別:DOM 型 XSS 攻擊中,取出和執行惡意程式碼由瀏覽器端完成,屬於前端 JavaScript 自身的安全漏洞,而其他兩種 XSS 都屬於服務端的安全漏洞。

如何防禦

XSS 攻擊有兩大要素:

  1. 攻擊者提交惡意程式碼。
  2. 瀏覽器執行惡意程式碼。

輸入過濾

對於明確的輸入型別,例如數字、URL、電話號碼、郵件地址等等內容,在使用者輸入和寫入資料庫前進行輸入過濾還是必要的。

輸入側過濾能夠在某些情況下解決特定的 XSS 問題,但會引入很大的不確定性和亂碼問題。 我們舉一個例子,一個正常的使用者輸入了 5 < 7 這個內容,在寫入資料庫前,被轉義,變成了 5 &lt; 7

問題是:在提交階段,我們並不確定內容要輸出到哪裡。

這裡的“並不確定內容要輸出到哪裡”有兩層含義:

  1. 使用者的輸入內容可能同時提供給前端和客戶端,而一旦經過了 escapeHTML(),客戶端顯示的內容就變成了亂碼( 5 &lt; 7 )。

  2. 在前端中,不同的位置所需的編碼也不同。

  3. 當 5 &lt; 7 作為 HTML 拼接頁面時,可以正常顯示:

```

5 < 7

```

  • 當 5 &lt; 7 通過 Ajax 返回,然後賦值給 JavaScript 的變數時,前端得到的字串就是轉義後的字元。這個內容不能直接用於 Vue 等模板的展示,也不能直接用於內容長度計算。不能用於標題、alert 等。

既然輸入過濾並非完全可靠,我們就要通過“防止瀏覽器執行惡意程式碼”來防範 XSS。這部分分為兩類:

  • 防止 HTML 中出現注入。
  • 防止 JavaScript 執行時,執行惡意程式碼。

預防儲存型和反射型 XSS 攻擊

儲存型和反射型 XSS 都是在服務端取出惡意程式碼後,插入到響應 HTML 裡的,攻擊者刻意編寫的“資料”被內嵌到“程式碼”中,被瀏覽器所執行。

預防這兩種漏洞,有兩種常見做法:

  • 改成純前端渲染,把程式碼和資料分隔開。
  • 對 HTML 做充分轉義。
純前端渲染

純前端渲染的過程:

  1. 瀏覽器先載入一個靜態 HTML,此 HTML 中不包含任何跟業務相關的資料。
  2. 然後瀏覽器執行 HTML 中的 JavaScript。
  3. JavaScript 通過 Ajax 載入業務資料,呼叫 DOM API 更新到頁面上。

但純前端渲染還需注意避免 DOM 型 XSS 漏洞(例如 onload 事件和 href 中的 javascript:xxx 等

轉義 HTML

如果拼接 HTML 是必要的,就需要採用合適的轉義庫,對 HTML 模板各處插入點進行充分的轉義。

常用的模板引擎,如 doT.js、ejs、FreeMarker 等,對於 HTML 轉義通常只有一個規則,就是把 & < > " ' / 這幾個字元轉義掉,確實能起到一定的 XSS 防護作用,但並不完善

預防 DOM 型 XSS 攻擊

DOM 型 XSS 攻擊,實際上就是網站前端 JavaScript 程式碼本身不夠嚴謹,把不可信的資料當作程式碼執行了。

在使用 .innerHTML.outerHTMLdocument.write() 時要特別小心,不要把不可信的資料作為 HTML 插到頁面上,而應儘量使用 .textContent.setAttribute() 等。

如果用 Vue/React 技術棧,並且不使用 v-html/dangerouslySetInnerHTML 功能,就在前端 render 階段避免 innerHTMLouterHTML 的 XSS 隱患。

DOM 中的內聯事件監聽器,如 locationonclickonerroronloadonmouseover 等,<a> 標籤的 href 屬性,JavaScript 的 eval()setTimeout()setInterval() 等,都能把字串作為程式碼執行。如果不可信的資料拼接到字串中傳遞給這些 API,很容易產生安全隱患,請務必避免。

其他安全措施

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

CSRF

跨站請求偽造(英語:Cross-site request forgery),也被稱為 one-click attack或者 session riding,通常縮寫為 CSRF 或者 XSRF, 是一種挾制使用者在當前已登入的 Web 應用程式上執行非本意的操作的攻擊方法。 跟XSS相比,XSS 利用的是使用者對指定網站的信任,CSRF 利用的是網站對使用者網頁瀏覽器的信任。

簡單點說,CSRF 就是利用使用者的登入態發起惡意請求。

CSRF 分為GET型,POST型(利用自動提交的表單)和連結型(不常見,只有開啟連結才會觸發)

一個典型的CSRF攻擊有著如下的流程:

  • 受害者登入a.com,並保留了登入憑證(Cookie)。
  • 攻擊者引誘受害者訪問了b.com。
  • b.com 向 a.com 傳送了一個請求:a.com/act=xx。瀏覽器會預設攜帶a.com的Cookie。
  • a.com接收到請求後,對請求進行驗證,並確認是受害者的憑證,誤以為是受害者自己傳送的請求。
  • a.com以受害者的名義執行了act=xx。
  • 攻擊完成,攻擊者在受害者不知情的情況下,冒充受害者,讓a.com執行了自己定義的操作。

如何防禦

同源檢測

既然CSRF大多來自第三方網站,那麼我們就直接禁止外域(或者不受信任的域名)對我們發起請求。

那麼問題來了,我們如何判斷請求是否來自外域呢?

在HTTP協議中,每一個非同步請求都會攜帶兩個Header,用於標記來源域名:

  • Origin Header
  • Referer Header

這兩個Header在瀏覽器發起請求時,大多數情況會自動帶上,並且不能由前端自定義內容。 伺服器可以通過解析這兩個Header中的域名,確定請求的來源域。

如果Origin存在,那麼直接使用Origin中的欄位確認來源域名就可以。

但是Origin在以下兩種情況下並不存在:

  • IE11同源策略:  IE 11 不會在跨站CORS請求上新增Origin標頭,Referer頭將仍然是唯一的標識。最根本原因是因為IE 11對同源的定義和其他瀏覽器有不同,有兩個主要的區別,可以參考MDN Same-origin_policy#IE_Exceptions**
  • 302重定向:  在302重定向之後Origin不包含在重定向的請求中,因為Origin可能會被認為是其他來源的敏感資訊。對於302重定向的情況來說都是定向到新的伺服器上的URL,因此瀏覽器不想將Origin洩漏到新的伺服器上。

origin不存在時使用Referer Header確定來源域名:

根據HTTP協議,在HTTP頭中有一個欄位叫Referer,記錄了該HTTP請求的來源地址。 對於Ajax請求,圖片和script等資源請求,Referer為發起請求的頁面地址。對於頁面跳轉,Referer為開啟頁面歷史記錄的前一個頁面地址。因此我們使用Referer中連結的Origin部分可以得知請求的來源域名。

這種方法並非萬無一失,Referer的值是由瀏覽器提供的,雖然HTTP協議上有明確的要求,但是每個瀏覽器對於Referer的具體實現可能有差別,並不能保證瀏覽器自身沒有安全漏洞。使用驗證 Referer 值的方法,就是把安全性都依賴於第三方(即瀏覽器)來保障,從理論上來講,這樣並不是很安全。在部分情況下,攻擊者可以隱藏,甚至修改自己請求的Referer。

2014年,W3C的Web應用安全工作組釋出了Referrer Policy草案,對瀏覽器該如何傳送Referer做了詳細的規定。截止現在新版瀏覽器大部分已經支援了這份草案,我們終於可以靈活地控制自己網站的Referer策略了。新版的Referrer Policy規定了五種Referer策略:No Referrer、No Referrer When Downgrade、Origin Only、Origin When Cross-origin、和 Unsafe URL。之前就存在的三種策略:never、default和always,在新標準裡換了個名稱。。他們的對應關係如下:

image.png

根據上面的表格因此需要把Referrer Policy的策略設定成same-origin,對於同源的連結和引用,會發送Referer,referer值為Host不帶Path;跨域訪問則不攜帶Referer。

設定Referrer Policy的方法有三種:

  1. 在CSP設定
  2. 頁面頭部增加meta標籤
  3. a標籤增加referrerpolicy屬性

上面說的這些比較多,但我們可以知道一個問題:攻擊者可以在自己的請求中隱藏Referer。

另外在以下情況下Referer沒有或者不可信:

  1. IE6、7下使用window.location.href=url進行介面的跳轉,會丟失Referer。
  2. IE6、7下使用window.open,也會缺失Referer。
  3. HTTPS頁面跳轉到HTTP頁面,所有瀏覽器Referer都丟失。
  4. 點選Flash上到達另外一個網站的時候,Referer的情況就比較雜亂,不太可信。

如果Origin和Referer都不存在,建議直接進行阻止,特別是如果您沒有使用隨機CSRF Token(參考下方)作為第二次檢查。

綜上所述:同源驗證是一個相對簡單的防範方法,能夠防範絕大多數的CSRF攻擊。但這並不是萬無一失的,對於安全性要求較高,或者有較多使用者輸入內容的網站,我們就要對關鍵的介面做額外的防護措施。

CSRF Token

前面講到CSRF的另一個特徵是,攻擊者無法直接竊取到使用者的資訊(Cookie,Header,網站內容等),僅僅是冒用Cookie中的資訊。

而CSRF攻擊之所以能夠成功,是因為伺服器誤把攻擊者傳送的請求當成了使用者自己的請求。那麼我們可以要求所有的使用者請求都攜帶一個CSRF攻擊者無法獲取到的Token。伺服器通過校驗請求是否攜帶正確的Token,來把正常的請求和攻擊的請求區分開,也可以防範CSRF的攻擊。

原理

CSRF Token的防護策略分為三個步驟:

1. 將CSRF Token輸出到頁面中

首先,使用者開啟頁面的時候,伺服器需要給這個使用者生成一個Token,該Token通過加密演算法對資料進行加密,一般Token都包括隨機字串和時間戳的組合,顯然在提交時Token不能再放在Cookie中了,否則又會被攻擊者冒用。因此,為了安全起見Token最好還是存在伺服器的Session中,之後在每次頁面載入時,使用JS遍歷整個DOM樹,對於DOM中所有的a和form標籤後加入Token。這樣可以解決大部分的請求,但是對於在頁面載入之後動態生成的HTML程式碼,這種方法就沒有作用,還需要程式設計師在編碼時手動新增Token。

2. 頁面提交的請求攜帶這個Token

對於GET請求,Token將附在請求地址之後,這樣URL 就變成 http://url/?csrftoken=tokenvalue%E3%80%82  而對於 POST 請求來說,要在 form 的最後加上:

<input type=”hidden” name=”csrftoken” value=”tokenvalue”/>

這樣,就把Token以引數的形式加入請求了。

3. 伺服器驗證Token是否正確

當用戶從客戶端得到了Token,再次提交給伺服器的時候,伺服器需要判斷Token的有效性,驗證過程是先解密Token,對比加密字串以及時間戳,如果加密字串一致且時間未過期,那麼這個Token就是有效的。

這種方法要比之前檢查Referer或者Origin要安全一些,Token可以在產生並放於Session之中,然後在每次請求時把Token從Session中拿出,與請求中的Token進行比對,但這種方法的比較麻煩的在於如何把Token以引數的形式加入請求。

Token是一個比較有效的CSRF防護方法,只要頁面沒有XSS漏洞洩露Token,那麼介面的CSRF攻擊就無法成功。

驗證碼和密碼其實也可以起到CSRF Token的作用哦,而且更安全。

Samesite Cookie屬性

防止CSRF攻擊的辦法已經有上面的預防措施。為了從源頭上解決這個問題,Google起草了一份草案來改進HTTP協議,那就是為Set-Cookie響應頭新增Samesite屬性,它用來標明這個 Cookie是個“同站 Cookie”,同站Cookie只能作為第一方Cookie,不能作為第三方Cookie,Samesite 有兩個屬性值,分別是 Strict 和 Lax,下面分別講解:

Samesite=Strict

這種稱為嚴格模式,表明這個 Cookie 在任何情況下都不可能作為第三方 Cookie,絕無例外。比如說 b.com 設定瞭如下 Cookie:

Set-Cookie: foo=1; Samesite=Strict Set-Cookie: bar=2; Samesite=Lax Set-Cookie: baz=3

我們在 a.com 下發起對 b.com 的任意請求,foo 這個 Cookie 都不會被包含在 Cookie 請求頭中,但 bar 會。舉個實際的例子就是,假如淘寶網站用來識別使用者登入與否的 Cookie 被設定成了 Samesite=Strict,那麼使用者從百度搜索頁面甚至天貓頁面的連結點選進入淘寶後,淘寶都不會是登入狀態,因為淘寶的伺服器不會接受到那個 Cookie,其它網站發起的對淘寶的任意請求都不會帶上那個 Cookie。

Samesite=Lax

這種稱為寬鬆模式,比 Strict 放寬了點限制:假如這個請求是這種請求(改變了當前頁面或者打開了新頁面)且同時是個GET請求,則這個Cookie可以作為第三方Cookie。比如說 b.com設定瞭如下Cookie:

Set-Cookie: foo=1; Samesite=Strict Set-Cookie: bar=2; Samesite=Lax Set-Cookie: baz=3

當用戶從 a.com 點選連結進入 b.com 時,foo 這個 Cookie 不會被包含在 Cookie 請求頭中,但 bar 和 baz 會,也就是說使用者在不同網站之間通過連結跳轉是不受影響了。但假如這個請求是從 a.com 發起的對 b.com 的非同步請求,或者頁面跳轉是通過表單的 post 提交觸發的,則bar也不會發送。

我們應該如何使用SamesiteCookie

如果SamesiteCookie被設定為Strict,瀏覽器在任何跨域請求中都不會攜帶Cookie,新標籤重新開啟也不攜帶,所以說CSRF攻擊基本沒有機會。

但是跳轉子域名或者是新標籤重新開啟剛登陸的網站,之前的Cookie都不會存在。尤其是有登入的網站,那麼我們新開啟一個標籤進入,或者跳轉到子域名的網站,都需要重新登入。對於使用者來講,可能體驗不會很好。

如果SamesiteCookie被設定為Lax,那麼其他網站通過頁面跳轉過來的時候可以使用Cookie,可以保障外域連線開啟頁面時使用者的登入狀態。但相應的,其安全性也比較低。

另外一個問題是Samesite的相容性不是很好,現階段除了從新版Chrome和Firefox支援以外,Safari以及iOS Safari都還不支援,現階段看來暫時還不能普及。

而且,SamesiteCookie目前有一個致命的缺陷:不支援子域。例如,種在topic.a.com下的Cookie,並不能使用a.com下種植的SamesiteCookie。這就導致了當我們網站有多個子域名時,不能使用SamesiteCookie在主域名儲存使用者登入資訊。每個子域名都需要使用者重新登入一次。

總之,SamesiteCookie是一個可能替代同源驗證的方案,但目前還並不成熟,其應用場景有待觀望。

總結

簡單總結一下上文的防護策略:

  • CSRF自動防禦策略:同源檢測(Origin 和 Referer 驗證)。
  • CSRF主動防禦措施:Token驗證 或者 雙重Cookie驗證 以及配合Samesite Cookie。
  • 保證頁面的冪等性,後端介面不要在GET頁面中做使用者操作。

xss是前端做還是後端做

防範儲存型和反射型 XSS 是後端的責任。而 DOM 型 XSS 攻擊不發生在後端,是前端的責任。防範 XSS 是需要後端和前端共同參與的系統工程。

轉義應該在輸出 HTML 時進行,而不是在提交使用者輸入時

安全問題主要參考這兩篇文章

說一下前端效能優化

效能優化在一面中問到過,可以直接跳轉檢視詳情

針對這個問題進行了如下詢問

什麼場景引起重繪,什麼場景引起重排

重繪和迴流是渲染步驟中的一小節,但是這兩個步驟對於效能影響很大。

  • 重繪是當節點需要更改外觀而不會影響佈局的,比如改變 color 會引起重繪
  • 迴流是佈局或者幾何屬性需要改變就會引起迴流。

迴流必定會發生重繪,重繪不一定會引發迴流。迴流所需的成本比重繪高的多,改變深層次的節點很可能導致父節點的一系列迴流。

業務中必須要重繪重排如何進行優化

  • 增加多個節點使用documentFragment
  • 使用 visibility 替換 display: none ,因為前者只會引起重繪,後者會引發迴流(改變了佈局)
  • 將頻繁執行的動畫變為圖層,圖層能夠阻止該節點回流影響別的元素。比如對於 video 標籤,瀏覽器會自動將該節點變為圖層。
  • 把 DOM 離線後修改,比如:先把 DOM 給 display:none (有一次 Reflow),然後你修改 100 次,然後再把它顯示出來

vue如何實現多次渲染的優化

非同步更新檢視:只要觀察到資料變化,Vue將開啟一個佇列,並緩衝在同一事件迴圈中發生的所有資料變化。如果同一個watcher被多次觸發,只會被推入到佇列中一次。這種在緩衝時去除重複資料對於避免不必要的計算和DOM操作上非常重要。然後,在下一個事件迴圈在“tick”中,Vue重新整理佇列並執行實際(已去重)的工作

程式碼題

js const result = ['1', '3', '3'].map(parseInt); // 這⾥會打印出什麼呢? console.log( result );

map的第一個引數callback的入參依次是: - 當前值 - 當前值下標 - 當前陣列

parseInt(解析一個字串並返回指定基數的十進位制整數)的入參依次是: - 要轉化的數字 - 第一個引數的基數: - 是一個2-36的數字,如果是1或者大於36會返回NaN,假如指定 0 或未指定,基數將會根據字串的值進行推算 - 如果第一個引數按照第二個定義的基數是無效的也會返回NaN

所以上述程式碼的返回結果為: js 1,NaN,NaN

程式碼題

```js function changeArg(x) { x = 200 }

let num = 100 changeArg(num) console.log('changeArg num', num) // 100

let obj = { name: '雙越' } changeArg(obj) console.log('changeArg obj', obj) // { name: '雙越' }

function changeArgProp(x) { x.name = '張三' } changeArgProp(obj) console.log('changeArgProp obj', obj) //{name:'張三'} ```

js中所有函式傳遞都是按值傳遞的,不會按引用傳遞。所謂的值,就是指直接儲存在變數上的值,如果把物件作為引數傳遞,那麼這個值就是這個物件的引用,而不是物件本身。這裡實際上是一個隱式的賦值過程,所以給函式傳遞引數時,相當於從一個變數賦值到另一個變數

其實可以理解為預設存在一個複製的過程:

```js changeArg(num) // 可以看做:x=num

changeArg(obj) // 可以看做:x=obj ```

程式碼題:實現一個函式,入參是一個fn,延遲5s執行,並且拿到返回值

```js function sleep(fn) { return new Promise(resolve => { setTimeout(() => { let res = fn() resolve(res) }, 5000) }) }

function f() { return 1 }

console.log(Date.now()) sleep(f).then(res => { console.log(res, Date.now()) }) ```