Upgrade to Pro — share decks privately, control downloads, hide ads and more …

今さら聞けないSPAのCORS対策の話

Avatar for Sota Sugiura Sota Sugiura
November 25, 2017

 今さら聞けないSPAのCORS対策の話

東京Node学園2017にて発表しました。

Avatar for Sota Sugiura

Sota Sugiura

November 25, 2017
Tweet

More Decks by Sota Sugiura

Other Decks in Technology

Transcript

  1. In this talk… • You can understand… • How Browser

    will work for CORS • Which HTTP headers should be provided for SPA • How we can manage cookie or some credential informations
  2. Same Origin Policy • Controlling interactions between different origins •

    The content in the origin will be protected from other origin • Important feature for security
  3. Cross Origin Fetch API http://localhost:9000 http://localhost:8000 const apiServer = 'http://localhost:8000';

    fetch(apiServer) .then(res => { console.log(res); }) .catch(err => { // Do nothing }); )551(&5
  4. What happened? • Browser blocked JavaScript to get HTTP response

    • Even if server returns response Resource Access Response
  5. It’s SOP • Some requests are rejected if target origin

    is different from current origin • Even if server send response, browser will hide data from JavaScript access
  6. CORS • Accessing to different origin with CORS • Controlling

    access permission with HTTP headers • Made for more flexible frontend interuction
  7. Cross Origin http://localhost:9000 http://localhost:8000 const http = require('http'); const PORT

    = 8000; const makeResForCORS = (response) => { response.setHeader( 'Access-Control-Allow-Origin', '*' ); }; const server = http.createServer((req, res) => { makeResForCORS(res); res.end('hello'); }); server.listen(PORT, () => { console.log(`Listening on ${PORT}`); });
  8. Cross Origin http://localhost:9000 http://localhost:8000 const http = require('http'); const PORT

    = 8000; const makeResForCORS = (response) => { response.setHeader( 'Access-Control-Allow-Origin', '*' ); }; const server = http.createServer((req, res) => { makeResForCORS(res); res.end('hello'); }); server.listen(PORT, () => { console.log(`Listening on ${PORT}`); });
  9. Cross Origin http://localhost:9000 http://localhost:8000 )5510, HTTP/1.1 200 OK Access-Control-Allow-Origin: *

    Date: Fri, 24 Nov 2017 08:41:45 GMT Connection: keep-alive Content-Length: 5 hello
  10. Cross Origin http://localhost:9000 http://localhost:8000 )5510, HTTP/1.1 200 OK Access-Control-Allow-Origin: *

    Date: Fri, 24 Nov 2017 08:41:45 GMT Connection: keep-alive Content-Length: 5 hello
  11. Cross Origin http://localhost:9000 http://localhost:8000 )5510, HTTP/1.1 200 OK Access-Control-Allow-Origin: *

    Date: Fri, 24 Nov 2017 08:41:45 GMT Connection: keep-alive Content-Length: 5 hello "MMPXBDDFTTJOHGSPNBOZPSJHJOT
  12. Specify more details • Access-Control-Allow-Origin • Origin which can access

    to resourse • Access-Control-Allow-Methods • HTTP methods client can use for cross origin access • and so on - I will explain others later :)
  13. Recap • Same Origin Policy • Security structure for protecting

    resource • CORS • You should set up for servers if you want to share resource across not same Origins
  14. Do you have these experiences? • Set Access-Control-Allow-Origin: * without

    any thinking • Using preflight, but it is not clear… • Creating SPA without understanding about CORS and other techniques around it
  15. We need to understand • CORS • preflight • Ajax

    with credentials • When we understand, we can explain it to server side engineer by own
  16. Let’s make sample SPA • API and static site Origins

    are different • We want to manage user session in some way • We want to make cross origin access more secure and more good performance
  17. Step 1 - Cross Origin Step 2 - User Session

    Step 3 - Optimize performance
  18. Step 1 - Cross Origin • Set up for CORS

    access • At least you need to set 3 kinds of headers • The value of headers depends on your application architecture
  19. You should specify • Access-Control-Allow-Origin • Origin which can access

    to resourse • Access-Control-Allow-Methods • HTTP methods client can use • Access-Control-Expose-Headers • HTTP headers client can send to server
  20. Set headers const http = require('http'); const PORT = 8000;

    const makeResForCORS = (response) => { response.setHeader('Access-Control-Allow-Origin', 'http://localhost:9000'); response.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,PATCH,OPTIONS'); response.setHeader('Access-Control-Expose-Headers', 'X-Custom-Header,X-Node-Festival'); }; const server = http.createServer((req, res) => { makeResForCORS(res); res.end('hello'); }); server.listen(PORT, () => { console.log(`Listening on ${PORT}`); });
  21. Set headers const http = require('http'); const PORT = 8000;

    const makeResForCORS = (response) => { response.setHeader('Access-Control-Allow-Origin', 'http://localhost:9000'); response.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,PATCH,OPTIONS'); response.setHeader('Access-Control-Expose-Headers', 'X-Custom-Header,X-Node-Festival'); }; const server = http.createServer((req, res) => { makeResForCORS(res); res.end('hello'); }); server.listen(PORT, () => { console.log(`Listening on ${PORT}`); });
  22. Step 2 - User Session • Want to identify user

    on server side • Authentication, permission • Controlling access to some endpoints
  23. Access Token? • Save in anywhere? • Local Storage? Or

    something like it? accessToken=abcd1234
  24. Not best • If there is XSS, it will be

    stolen easily • We have no way to protect local data from JavaScript • And it is not easy to implement • Is will be expired or not? • How to implement sign in/sign out logic?
  25. Cookie is better way • We don’t need to implement

    special logic • We can protect cookie from XSS • Surely, we MUST use https
  26. Do you worry document.cookie? • You MUST set this header

    • Set-Cookie: HttpOnly; Secure; • HttpOnly - Cookie won’t be accessed from JavaScript • Secure - Cookie will be sent on only HTTPS connection
  27. Ajax with credentials • When you want to send credentials

    such as cookie, you need to specify option • Server also need to specify that credentials is allowed to sent
  28. Ajax with credentials const apiServer = 'http://localhost:8000'; // Fetch API

    fetch(apiServer, { credentials: 'include', }) .then(res => { console.log(res); }) .catch(err => { // Do nothing }); // XMLHttpRequest const xhr = new XMLHttpRequest(); xhr.open('GET', apiServer, true); xhr.withCredentials = true; xhr.onload = (res) => { console.log(res); }; xhr.send();
  29. Ajax with credentials const apiServer = 'http://localhost:8000'; // Fetch API

    fetch(apiServer, { credentials: 'include', }) .then(res => { console.log(res); }) .catch(err => { // Do nothing }); // XHLHttpRequest const xhr = new XMLHttpRequest(); xhr.open('GET', apiServer, true); xhr.withCredentials = true; xhr.onload = (res) => { console.log(res); }; xhr.send();
  30. Server side for sending credentials const http = require('http'); const

    PORT = 8000; const makeResForCORS = (response) => { response.setHeader('Access-Control-Allow-Origin', '*'); response.setHeader('Access-Control-Allow-Credentials', 'true'); }; const server = http.createServer((req, res) => { makeResForCORS(res); res.end('hello'); }); server.listen(PORT, () => { console.log(`Listening on ${PORT}`); });
  31. Server side for sending credentials const http = require('http'); const

    PORT = 8000; const makeResForCORS = (response) => { response.setHeader('Access-Control-Allow-Origin', '*'); response.setHeader('Access-Control-Allow-Credentials', 'true'); }; const server = http.createServer((req, res) => { makeResForCORS(res); res.end('hello'); }); server.listen(PORT, () => { console.log(`Listening on ${PORT}`); });
  32. “*” is dead… • You can’t define ‘*’ for Access-Control-

    Allow-Origin • Even if you don’t send credentials cross origin, ‘*’ does not make sense • Also for security, let’s define specific origin!
  33. Server side for sending credentials const http = require('http'); const

    PORT = 8000; const makeResForCORS = (response) => { response.setHeader('Access-Control-Allow-Origin', 'http://localhost:9000'); response.setHeader('Access-Control-Allow-Credentials', 'true'); }; const server = http.createServer((req, res) => { makeResForCORS(res); res.end('hello'); }); server.listen(PORT, () => { console.log(`Listening on ${PORT}`); });
  34. Step 3 - Optimize performance • Before optimizing your application’s

    performance, you need to know about preflight request
  35. Preflight Request • Pre request before accessing to other origin

    • It is sent by browser automatically • It means if browser send preflight request, it takes 2RTT to access to the resource
  36. Preflight Request OPTIONS / HTTP/1.1 Host: localhost:8000 User-Agent: Mozilla/5.0 (Macintosh;

    Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/ 62.0.3202.94 Safari/537.3620081130 Minefield/3.1b3pre Accept: */* Accept-Language: ja,en-US;q=0.9,en;q=0.8 Accept-Encoding: gzip, deflate, br Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Connection: keep-alive Origin: http://localhost:9000
 Referer: http://localhost:9000/ Access-Control-Request-Method: PUT Access-Control-Request-Headers: x-custom-header
  37. Preflight Request OPTIONS / HTTP/1.1 Host: localhost:8000 User-Agent: Mozilla/5.0 (Macintosh;

    Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/ 62.0.3202.94 Safari/537.3620081130 Minefield/3.1b3pre Accept: */* Accept-Language: ja,en-US;q=0.9,en;q=0.8 Accept-Encoding: gzip, deflate, br Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Connection: keep-alive Origin: http://localhost:9000
 Referer: http://localhost:9000/ Access-Control-Request-Method: PUT Access-Control-Request-Headers: x-custom-header
  38. Preflight Request OPTIONS / HTTP/1.1 Host: localhost:8000 User-Agent: Mozilla/5.0 (Macintosh;

    Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/ 62.0.3202.94 Safari/537.3620081130 Minefield/3.1b3pre Accept: */* Accept-Language: ja,en-US;q=0.9,en;q=0.8 Accept-Encoding: gzip, deflate, br Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Connection: keep-alive Origin: http://localhost:9000
 Referer: http://localhost:9000/ Access-Control-Request-Method: PUT Access-Control-Request-Headers: x-custom-header
  39. Response for Preflight HTTP/1.1 200 OK Access-Control-Allow-Origin: http://localhost:9000 Access-Control-Allow-Methods: GET,POST,PUT,DELETE,PATCH,OPTIONS

    Access-Control-Allow-Headers: X-Custom-Header,X-Node- Festival Access-Control-Allow-Credentials: true Access-Control-Max-Age: 100 Date: Fri, 24 Nov 2017 17:23:24 GMT Connection: keep-alive
  40. Response for Preflight HTTP/1.1 200 OK Access-Control-Allow-Origin: http://localhost:9000 Access-Control-Allow-Methods: GET,POST,PUT,DELETE,PATCH,OPTIONS

    Access-Control-Allow-Headers: X-Custom-Header,X-Node- Festival Access-Control-Allow-Credentials: true Access-Control-Max-Age: 100 Date: Fri, 24 Nov 2017 17:23:24 GMT Connection: keep-alive
  41. When? • When you want to use specific HTTP methods

    • ex) PUT, DELETE, PATCH, OPTIONS • When you want to use custom HTTP header • When you want to specify Content-Type header except some values
  42. Decreasing latency • Let’s make your app 2RTT to 1RTT

    • Use only GET, POST method • Not using custom HTTP headers • Set specific value to Content-Type
  43. Do not forget CSRF • If you avoid using preflight,

    you need to consider about CSRF • Even if browser protect resource from JavaScript, request will be sent to server Bad Request Response
  44. For CSRF • Adding origin checking on server • Using

    preflight request • The thing you should think about is not to exec request before any checking
  45. CSRF http://localhost:10000 http://localhost:8000 #BE3FRVFTU const http = require('http'); const PORT

    = 8000; const TARGET_ORIGIN = 'http://localhost:9000'; const makeResForCORS = (response) => { response.setHeader( 'Access-Control-Allow-Origin', TARGET_ORIGIN ); }; const server = http.createServer((req, res) => { makeResForCORS(res); if (req.headers.origin !== TARGET_ORIGIN) { res.end('invalid request'); return; } res.end('hello'); }); server.listen(PORT, () => { console.log(`Listening on ${PORT}`); });
  46. CSRF http://localhost:10000 http://localhost:8000 #BE3FRVFTU const http = require('http'); const PORT

    = 8000; const TARGET_ORIGIN = 'http://localhost:9000'; const makeResForCORS = (response) => { response.setHeader( 'Access-Control-Allow-Origin', TARGET_ORIGIN ); }; const server = http.createServer((req, res) => { makeResForCORS(res); if (req.headers.origin !== TARGET_ORIGIN) { res.end('invalid request'); return; } res.end('hello'); }); server.listen(PORT, () => { console.log(`Listening on ${PORT}`); });
  47. You need to use preflight? • In some case, you

    can’t avoid using preflight request • Don’t worry, you can optimize around preflight request
  48. Cache Preflight http://localhost:9000 http://localhost:8000 3FTQPOTF HTTP/1.1 200 OK Access-Control-Allow-Origin: http://

    localhost:9000 Access-Control-Allow-Methods: GET,POST,PUT,DELETE,PATCH,OPTIONS Access-Control-Allow-Headers: X- Custom-Header,X-Node-Festival Access-Control-Allow-Credentials: true Access-Control-Max-Age: 100 Date: Fri, 24 Nov 2017 17:23:24 GMT Connection: keep-alive
  49. Cache Preflight http://localhost:9000 http://localhost:8000 3FTQPOTF HTTP/1.1 200 OK Access-Control-Allow-Origin: http://

    localhost:9000 Access-Control-Allow-Methods: GET,POST,PUT,DELETE,PATCH,OPTIONS Access-Control-Allow-Headers: X- Custom-Header,X-Node-Festival Access-Control-Allow-Credentials: true Access-Control-Max-Age: 100 Date: Fri, 24 Nov 2017 17:23:24 GMT Connection: keep-alive
  50. Cache Preflight http://localhost:9000 http://localhost:8000 3FTQPOTF HTTP/1.1 200 OK Access-Control-Allow-Origin: http://

    localhost:9000 Access-Control-Allow-Methods: GET,POST,PUT,DELETE,PATCH,OPTIONS Access-Control-Allow-Headers: X- Custom-Header,X-Node-Festival Access-Control-Allow-Credentials: true Access-Control-Max-Age: 100 Date: Fri, 24 Nov 2017 17:23:24 GMT Connection: keep-alive $BDIFQSFqJHIUSFTVMUGPSTFDPOET
  51. Access-Control-Max-Age • You can let browser to cache preflight result

    • But max cache time is limited • Chrome - 10 minutes • Firefox - 24 hours • Specify value for your app’s spec
  52. That’s all • ✅ Basic settings for CORS • ✅

    Ajax with credentials • ✅ Avoid using Preflight Request • ✅ Or cache request for optimizing
  53. Summary • CORS is for interactions between different Origins •

    When you write frontend code like SPA… • You need to understand around CORS • You can optimize communication with different origin