Share
Embed
フロントエンド開発のためのセキュリティ入門
Developers Summit 2023 10-A-4 「フロントエンド開発のためのセキュリティ入門」の発表資料です。
https://event.shoeisha.jp/devsumi/20230209/session/4176/
「HTTPS化」「CORS」「XSS」「脆弱なライブラリのチェック」について説明しています。
Masashi Hirano
February 10, 2023
Transcript
99 slides
-
ฏ ণ࢜ / Masashi Hirano Frontend Developer @shisama_ shisama Node.js
Core Collaborator ࣾձਓେֶੜ@UoPeople -
WebΞϓϦέʔγϣϯͰൃੜ͢ΔηΩϡϦςΟϦεΫ 08"415PQ " ΞΫηε੍ޚͷෆඋ " ҉߸Խͷࣦഊ " ΠϯδΣΫγϣϯ " ҆શ͕֬ೝ͞Εͳ͍ෆ҆ͳઃܭ
" ηΩϡϦςΟͷઃఆϛε " ੬ऑͰݹ͘ͳͬͨίϯϙʔωϯτ " ࣝผͱೝূͷࣦഊ " ιϑτΣΞͱσʔλͷ߹ੑͷෆ۩߹ " ηΩϡϦςΟϩάͱϞχλϦϯάͷࣦഊ " αʔόʔαΠυϦΫΤετϑΥʔδΣϦ -
• ҉߸Խͷࣦഊ ➡ ௨৴σʔλͷ҉߸Խ • ΞΫηε੍ޚͷෆඋ ➡ CORSΛͬͨΞΫηε੍ޚ • ΠϯδΣΫγϣϯ
➡ XSSରࡦ • ੬ऑͰݹ͘ͳͬͨίϯϙʔωϯτ ➡ ੬ऑͳϥΠϒϥϦͷνΣοΫ ࠓճ͓͢ΔηΩϡϦςΟϦεΫ -
• ҉߸Խͷࣦഊ ➡ ௨৴σʔλͷ҉߸Խ • ΞΫηε੍ޚͷෆඋ ➡ CORSΛͬͨΞΫηε੍ޚ • ΠϯδΣΫγϣϯ
➡ XSSରࡦ • ੬ऑͰݹ͘ͳͬͨίϯϙʔωϯτ ➡ ੬ऑͳϥΠϒϥϦͷνΣοΫ ࠓճ͓͢ΔηΩϡϦςΟϦεΫ -
Webϖʔδͷදࣔϒϥβͱαʔόͷ௨৴͔Β͡·Δ HTML JS WebαΠτͷαʔό site.example ଞͷWebαΠτCDNͷαʔό JSON Ϣʔβ͕WebαΠτΞΫηε HTMLͳͲͷϦιʔεΛૹ৴
WebαΠτͱผͷαʔόϦΫΤετ ϦιʔεΛૹ৴ -
Webϖʔδͷදࣔϒϥβͱαʔόͷ௨৴͔Β͡·Δ HTML JS WebαΠτͷαʔό site.example ଞͷWebαΠτCDNͷαʔό JSON Ϣʔβ͕WebαΠτΞΫηε HTMLͳͲͷϦιʔεΛૹ৴
WebαΠτͱผͷαʔόϦΫΤετ ϦιʔεΛૹ৴ ௨৴ʹ)551ͱ͍͏ϓϩτίϧ ͕ΘΕ͍ͯΔ ௨৴్தͰ߈ܸΛ͞ΕΔͱɺϒϥβ αʔόͰରࡦ͍͍ͯͯ͠ແҙຯ -
Mixed Contentͷةݥੑ • 1ͭͷϖʔδ্ͰHTTPSͰ৴͞ΕͨϦιʔεͱHTTPͰ৴͞ΕͨϦ ιʔε͕ࠞࡏ͢Δ͜ͱΛMixed ContentͱݺͿ • HTML͕HTTPSʹ৴͞Ε͍ͯͯαϒϦιʔεͷJavaScriptͳͲ͕ HTTPͰ৴͞Ε͍ͯΔͱɺ͔ͦ͜Β߈ܸ͞ΕΔՄೳੑ͕͋Δ •
Chrome DevToolsͳͲΛ͍ͳ͕ΒMixed ContentΛमਖ਼͠ͳ͚Ε ͳΒͳ͍ -
• ҉߸Խͷࣦഊ ➡ ௨৴σʔλͷ҉߸Խ • ΞΫηε੍ޚͷෆඋ ➡ CORSΛͬͨΞΫηε੍ޚ • ΠϯδΣΫγϣϯ
➡ XSSରࡦ • ੬ऑͰݹ͘ͳͬͨίϯϙʔωϯτ ➡ ੬ऑͳϥΠϒϥϦͷνΣοΫ ࠓճ͓͢ΔηΩϡϦςΟϦεΫ -
Same Origin PolicyʹΑΔ੍ݶର • JavaScriptʢfetchɺXHRʣΛͬͨΫϩεΦϦδϯͷϦΫΤετ • JavaScriptΛͬͨiframeͷΫϩεΦϦδϯͷϖʔδͷΞΫηε • ΫϩεΦϦδϯͷը૾ΛಡΈࠐΜͩ<canvas>ཁૉͷσʔλͷΞΫ ηε
• Web StorageIndexedDBʹอଘ͞ΕͨΫϩεΦϦδϯͷσʔλͷ ΞΫηε -
CORSͷ՝ • σʔλΛऔͬͯ͘Δ͚ͩͳΒ͜͜·Ͱͷઆ໌Ͱͳ͍ • σʔλΛߋ৽ɾআͳͲ෭࡞༻Λى͜͢ϦΫΤετͷ߹ɺϦΫΤ ετࣗମΛ͙ඞཁ͕͋Δ • ͨͱ͑σʔλΛআ͢ΔϦΫΤετ͕ૹ৴͞Εͯαʔόಧ͘ͱ αʔόϨεϙϯεΛฦ͢લʹσʔλΛআͯ͠͠·͏͔͠Εͳ͍ •
Access-Control-Allow-OriginϔομͰɺϦΫΤετΛड͚औͬͨ αʔόͷಈ࡞·Ͱ੍ޚͰ͖ͳ͍ -
CORSͷ՝ ᶃ σʔλΛআ͢ΔϦΫΤετΛૹ৴ ᶅ Access-Control-Allow-OriginΛؚΉ ϨεϙϯεΛૹ৴ ᶄ σʔλͷআॲཧΛ࣮ߦ ᶆ
ϨεϙϯεΛഁغ ϨεϙϯεσʔλഁغͰ͖ͯɺ σʔλͷআࢭΊΕ͍ͯͳ͍ -
• ҉߸Խͷࣦഊ ➡ ௨৴σʔλͷ҉߸Խ • ΞΫηε੍ޚͷෆඋ ➡ CORSΛͬͨΞΫηε੍ޚ • ΠϯδΣΫγϣϯ
➡ XSSରࡦ • ੬ऑͰݹ͘ͳͬͨίϯϙʔωϯτ ➡ ੬ऑͳϥΠϒϥϦͷνΣοΫ ࠓճ͓͢ΔηΩϡϦςΟϦεΫ ଞʹ42-ΠϯδΣΫγϣϯͳͲ͕ ͋Δ͕ࠓ944͚ͩઆ໌͠·͢ -
XSSͷڴҖ • ػີใͷ࿙͍͑ • ِใͷදࣔ • ҙਤ͠ͳ͍ૢ࡞ͷ࣮ߦ • ηογϣϯใୣऔʹΑΔɺͳΓ͢·͠ •
ِͷೖྗϑΥʔϜදࣔʹΑΔϑΟογϯά -
3छྨͷXSS • ࣹܕXSSʢRe fl ected XSS) • ੵܕXSSʢStored XSSʣ •
DOM-based XSS -
ࣹܕXSS (Re fl ected XSS) ᶃ ߈ܸऀ͕༻ҙͨ͠ϖʔδʹΞΫηε ᶄ εΫϦϓτΛύϥϝʔλʹ ͯ͠ରαΠτʹભҠ
<script>alert("XSS")</script> ?token=“”/><script>alert(“XSS")</script><div ᶅ εΫϦϓτ͕ ຒΊࠐ·ΕͨHTML -
ੵܕXSS (Stored XSS) ᶃ ϑΥʔϜ͔ΒPOST ᶄ POST͞Εͨ Λͦͷ··อଘ <script>alert(“xss")</script> <li
class="item"><script>alert(1)</script></li> ᶅ ଞͷϢʔβʔ͕αΠτʹΞΫηε ᶆ DBͷΛHTMLʹө -
DOM-based XSS const hash = decodeURIComponent(location.hash.slice(1)); document.querySelector('#result').innerHTML = hash; <img
src=x onerror=alert('XSS')> ϩέʔγϣϯϋογϡͷΛ JavaScriptͰૠೖ -
DOM-based XSSͷಛ • ϒϥβ্ͷJavaScriptΛͬͨDOMૢ࡞͕ݪҼͰൃੜ͢Δ • ଞͷ2ͭͱҧ͍ϑϩϯτΤϯυͷίʔυͷෆඋʹΑͬͯൃੜ͢Δ • αʔόΛհͣ͞ʹ߈ཱܸ͕͢ΔͨΊɺ߈ܸͷݕϩάऔಘ͕͠ ͍ •
ϑϩϯτΤϯυͷJavaScriptDevTools͔ΒݟΔ͜ͱ͕ग़དྷΔͨΊɺ ߈ܸऀ੬ऑੑΛൃݟ͍͢͠ -
DOM-based XSSͷݪҼ const hash = decodeURIComponent(location.hash.slice(1)); document.querySelector('#result').innerHTML = hash; ιʔε
(Source) γϯΫ (Sink) දతͳSink - innerHTML - location.href - document.write - jQuery() ͳͲ දతͳSource - location.hash - location.href - document.referrer - IndexedDB ͳͲ -
จࣈྻͷΤεέʔϓ <script> cookieHijack() </script> <script> cookieHijack() </script> & < มલ
มޙ > “ ‘ & < > " ' Τεέʔϓॲཧ -
ReactͳͲͷϑϨʔϜϫʔΫΛ͏ • ReactɺVueɺAngularɺSvelteͳͲͷ༗໊ͳϥΠϒϥϦ/ϑϨʔϜϫʔΫΤεέʔϓॲཧΛ ࣗಈͰͬͯ͘ΕΔ • ։ൃऀΤεέʔϓॲཧΛ͢Δඞཁͳ͍ • ͨͩ͠ɺҰ෦ͷAPIΛ͏͜ͱͰXSSൃੜ͢Δ͜ͱ͕͋Γ·͢ • dangerouslySetInnerHTMLinnerHTMLͱಉ͘͡จࣈྻΛͦͷ··HTMLͱͯ͠ೝࣝ͢Δ
ʢReactʣ • ޙड़ͷjavascript:εΩʔϜʹΑΔXSSʹରԠ͍ͯ͠ͳ͍ʢReactʣ • ίϯιʔϧʹܯࠂग़Δ -
javascript:εΩʔϜͷURLΛຒΊࠐ·ͳ͍ • ͨͱ͑ɺϦϯΫͷURLΛೖྗͰ͖ΔϑΥʔϜ͕͋ͬͨͱ͢Δ • ϑΥʔϜʹhttps://example.comͱೖྗͨ͠Βhttps://example.com ͷϦϯΫ͕ੜ͞ΕΔͱ͢Δ <div className=“App"> <form> <input
value={title} onChange={onChangeTitle} /> <input value={url} onChange={onChangeUrl} /> </form> <a href={url}>{title}</a> </div> ҙͷ63-ΛઃఆՄೳͱ͢Δ -
javascript:εΩʔϜͷURLΛຒΊࠐ·ͳ͍ • ϑΥʔϜʹjavascript:alert(“xss”)ͱೖྗ͞ΕΔͱɺΫϦοΫͨ͠ͱ͖ ʹalert(“xss”)͕࣮ߦ͞ΕΔϦϯΫ͕Ͱ͖ͯ͠·͏ const App = (props) => (
<div className=“App"> <form> <input value={title} onChange={onChangeTitle} /> <input value={url} onChange={onChangeUrl} /> </form> <a href=“javascript:alert(“xss")">click me!</a> KBWBTDSJQUεΩʔϜΛ͑ҙͷ εΫϦϓτΛૠೖͰ͖Δ -
javascript:εΩʔϜͷURLΛຒΊࠐ·ͳ͍ const Link = props => { const protocol =
new URL(props.url).protocol; const safeUrl = /^https?:/.test(protocol) ? props.url : ""; return <a href={safeUrl}>{props.children}</a>; }; const App = (props) => { return ( <div className="App"> <Link url={url}>{title}</Link> </div> ); }; IUUQ·ͨIUUQT͔Β͡·Δ63-͚ͩΛ ઃఆ͢ΔΑ͏ʹϑΟϧλϦϯά͢Δ -
DOMPurifyΛͬͨةݥͳจࣈྻͷআڈ • DOMPurifyOSSͷϥΠϒϥϦ • npmCDN͔Βμϯϩʔυͯ͠͏͜ͱ͕Ͱ͖Δ • จࣈྻ͔ΒXSSΛى͜͢ةݥͳจࣈྻΛআͯ͘͠ΕΔ const untrustedStr =
location.hash; // <img src=x onerror="javascript:alert(1)" /> const trustedStr = DOMPurify.sanitize(untrustedStr); $('#foo').innerHTML = trustedStr; // <img src=x /> -
Sanitizer APIΛͬͨةݥͳจࣈྻͷআڈ • ϒϥβʹΈࠐ·Ε͍ͯΔAPI • Ͱ͖Δ͜ͱDOMPurifyͱࣅ͍ͯͯةݥͳจࣈྻΛআڈ͢Δ const untrustedStr = location.hash;
// <img src=x onerror="javascript:alert(1)" /> const sanitizer = new Sanitizer(untrustedStr); const target = document.getElementById(untrustedStr); target.setHTML(untrustedStr, { sanitizer }); // <img src=x /> -
Content Security Policyͷ༻ํ๏ • HTTPϨεϙϯεϔομʔʹՃ͢Δ • ϦιʔεͷऔಘઌεΫϦϓτͷ࣮ߦΛࢦఆͨ͠ιʔεͷΈʹ੍ݶͰ͖Δ • script-src ‘self’;
img-src *.trusted.comͷ ͷΑ͏ʹෳઃఆՄೳ ▼ Response Headers content-security-policy: script-src ‘self’ *.trusted.com σΟϨΫςΟϒ ιʔε script-src ‘self’; img-src *.trusted.com -
Content Security Policyͷʹ͍ͭͯ content-security-policy: script-src ‘self’ *.trusted.com : εΫϦϓτͷ࣮ߦ੍ݶ :
ڐՄ͢ΔιʔεΛࢦఆ script-src ‘self’ *.trusted.com Ωʔϫʔυ ϗετ -
දతͳσΟϨΫςΟϒ σΟϨΫςΟϒ DPOOFDUTSD 9)3ɺ8FC4PDLFUͳͲͷ੍ݶ EFGBVMUTSD ଞͷϑΣονσΟϨΫςΟϒͷϑΥʔϧόοΫ JNHTSD ը૾ͷಡΈࠐΈઌΛ੍ݶ NFEJBTSD BVEJPWJEFPͷಡΈࠐΈઌΛ੍ݶ
TDSJQUTSD εΫϦϓτͷ࣮ߦॴΛ੍ݶ TUZMFTSD ελΠϧͷద༻ॴΛ੍ݶ -
<meta>͔ΒCSPΛ༗ޮʹͰ͖Δ • HTMLͷ<meta>λάͰࢦఆͰ͖ΔͷͰϑϩϯτΤϯυ͚ͩͰ࣮Մೳ • <meta>ΑΓઌʹઃఆ͞Εͨͷ্ॻ͖ෆՄ • <meta>ΑΓઌʹಡΈࠐ·ΕͨϦιʔεεΫϦϓτʹద༻Ͱ͖ͳ͍ • <meta>ͰઃఆͰ͖ͳ͍σΟϨΫςΟϒ͕͋Δ e.g.
report-only <meta http-equiv="Content-Security-Policy" content="script-src 'self'"> -
Strict CSP • ҆શʹΠϯϥΠϯεΫϦϓτΛ࣮ߦͰ͖Δnonce·ͨhashΛϕʔε ͱͨ͠Strict CSPͱ͍͏ઃఆ͕GoogleʹΑͬͯਪ͞Ε͍ͯΔ Content-Security-Policy: script-src 'nonce-tXCHNF14TxHbBvCj3G0WmQ==' 'strict-dynamic'
https: 'unsafe-inline'; object-src 'none'; base-uri 'none'; -
nonceͷҙ • nonceͷαʔόʔଆͰϦΫΤετ͝ͱʹมߋ͢Δ͜ͱ • ਪଌ͞Εͳ͍ϥϯμϜͳʹ͢Δ͜ͱ • scriptλάΛಈతʹੜͰ͖ͳ͍ content-security-policy: script-src ‘nonce-EDNnf03nceIOfn39fn3e9h3sdfa'
<script nonce=“EDNnf03nceIOfn39fn3e9h3sdfa">alert("OK")</script> -
strict-dynamic content-security-policy: script-src ‘nonce-EDNnf03nceIOfn39fn3e9h3sdfa’ ‘strict-dynamic’ <script nonce="EDNnf03nceIOfn39fn3e9h3sdfa"> const script =
document.createElement('script'); script.src = ‘/static/js/main.js’; document.head.appendChild(script); </script> QBSTFSJOTFSUFEͰͳ͍TDSJQUͷಈతੜ͕Մೳ JOOFS)5.-ͳͲγϯΫʹͳΔؔېࢭ͞Ε͍ͯΔ ※parser-inserted = HTMLύʔαʔXMLύʔαʔʹΑͬͯૠೖ͞ΕΔ͜ͱ -
CSPͷReport-OnlyϞʔυ • ࣮ࡍͷCSPΛద༻ͤͣʹϨϙʔτ͚ͩૹΔ͜ͱ͕Մೳ • CSPͰڐՄ͞Ε͍ͯͳ͍߹ɺϨϙʔτΛPOST͢Δ • CSPͷຊ൪ಋೖલʹӨڹΛ֬ೝ͢Δ͜ͱ͕Մೳ • <meta>λάͰͷઃఆෆՄ ▼
Response Headers content-security-policy-report-only: script-src ‘self’ *.trusted.com report-uri /csp-report -
Ϩϙʔτͷྫ { "csp-report": { "document-uri": "http://example.com/index.html", "referrer": "", "blocked-uri": "http://invalid-cdn.com/js/react.js",
"violated-directive": "script-src self *.trusted.com*", "original-policy": "script-src 'self' *.trusted.com; report-uri /csp-report/", "disposition": "report" } } -
• ҉߸Խͷࣦഊ ➡ ௨৴σʔλͷ҉߸Խ • ΞΫηε੍ޚͷෆඋ ➡ CORSΛͬͨΞΫηε੍ޚ • ΠϯδΣΫγϣϯ
➡ XSSରࡦ • ੬ऑͰݹ͘ͳͬͨίϯϙʔωϯτ ➡ ੬ऑͳϥΠϒϥϦͷνΣοΫ ࠓճ͓͢ΔηΩϡϦςΟϦεΫ -
npm auditΛ͏ • npmʹඪ४ͰΈࠐ·Ε͍ͯΔίϚϯυπʔϧ • ར༻͍ͯ͠Δnpmύοέʔδͷ੬ऑੑΛஅͯ͘͠ΕΔ • npm installΛ͢Δͱ͖ʹࣗಈͰ࣮ߦ͞ΕΔ •
CIͰ࣮ߦ͢ΕࣗಈͰ੬ऑੑͷ͋ΔϥΠϒϥϦͷར༻Λ͙͜ͱ͕Ͱ͖Δ • npm audit fi xίϚϯυͰ੬ऑͳϥΠϒϥϦͷΞοϓσʔτΛҰׅͰߦ͏͜ͱ ͕Ͱ͖Δ