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

046baac588d91fd78a85b189847a151d?s=47 Sota Sugiura
November 25, 2017

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

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

046baac588d91fd78a85b189847a151d?s=128

Sota Sugiura

November 25, 2017
Tweet

Transcript

  1. 今さら聞けないSPAのCORS対策の話 Making SPA with thinking about CORS @sota1235

  2. console.log(me) • Sota Sugiura • @sota1235 • Mercari, Inc. •

    My dream is to be JavaScript
  3. By the way

  4. None
  5. 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
  6. CORS

  7. Cross Origin Resource Sharing

  8. Cross Origin Resource Sharing

  9. What’s origin?

  10. Origin • Defined from 3 part • Scheme • Host

    • Port
  11. Origin https://example.com:80

  12. Origin https://example.com:80 Scheme Host Port

  13. Origin https://example.com:80 http://example.com:80

  14. Origin https://example.com:80 http://example.com:80 Different scheme

  15. Origin https://example.com:80 http://example.com:80 Different scheme then different Origin

  16. Origin ! IUUQTIPHFDPN IUUQTIPHFDPNGVHBIUNM IUUQTIPHFDPN IUUQIPHFDPN IUUQIPHFDPN IUUQIPHFDPN IUUQIPHFDPN IUUQNPHFIPHFDPN

  17. Origin for what?

  18. Same Origin Policy

  19. Same Origin Policy • Controlling interactions between different origins •

    The content in the origin will be protected from other origin • Important feature for security
  20. SOP for… • XMLHttpRequest • Fetch API • Images drawn

    on Canvas • and so on
  21. Cross Origin Fetch API http://localhost:9000 http://localhost:8000

  22. Cross Origin Fetch API http://localhost:9000 http://localhost:8000 #SPXTFS "1*

  23. Cross Origin Fetch API http://localhost:9000 http://localhost:8000 /PUTBNF0SJHJO

  24. Cross Origin Fetch API http://localhost:9000 http://localhost:8000 )551(&5

  25. 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
  26. Error occurred

  27. What happened? • Browser blocked JavaScript to get HTTP response

    Resource Access Response
  28. What happened? • Browser blocked JavaScript to get HTTP response

    • Even if server returns response Resource Access Response
  29. 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
  30. So, CORS

  31. CORS • Accessing to different origin with CORS • Controlling

    access permission with HTTP headers • Made for more flexible frontend interuction
  32. Cross Origin http://localhost:9000 http://localhost:8000

  33. Cross Origin http://localhost:9000 http://localhost:8000 const apiServer = 'http://localhost:8000'; fetch(apiServer) .then(res

    => { console.log(res); }) .catch(err => { // Do nothing });
  34. 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}`); });
  35. 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}`); });
  36. Cross Origin http://localhost:9000 http://localhost:8000 )551(&5 const apiServer = 'http://localhost:8000'; fetch(apiServer)

    .then(res => { console.log(res); }) .catch(err => { // Do nothing });
  37. 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
  38. 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
  39. 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
  40. 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 :)
  41. 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
  42. BTW, Today’s theme is CORS with SPA

  43. 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
  44. Not good

  45. We need to understand • CORS • preflight • Ajax

    with credentials • When we understand, we can explain it to server side engineer by own
  46. 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
  47. Let’s make sample SPA http://localhost:9000 http://localhost:8000 眢 4FDVSF 眢 ,FFQVTFSTFTTJPO

    眢 (PPEQFSGPSNBODF #SPXTFS "1*
  48. Step 1 - Cross Origin Step 2 - User Session

    Step 3 - Optimize performance
  49. 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
  50. 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
  51. 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}`); });
  52. 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}`); });
  53. Step 2 - User Session • Want to identify user

    on server side • Authentication, permission • Controlling access to some endpoints
  54. How to mange session? • Access token? • Cookie? •

    Or other way?
  55. Access Token? • Save in anywhere? • Local Storage? Or

    something like it? accessToken=abcd1234
  56. 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?
  57. Cookie is better way • We don’t need to implement

    special logic • We can protect cookie from XSS • Surely, we MUST use https
  58. 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
  59. Let’s use cookie! But… 5IFSFJTOPcookieIFBEFS

  60. 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
  61. 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();
  62. 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();
  63. 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}`); });
  64. 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}`); });
  65. mmm, still not perfect

  66. “*” 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!
  67. 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}`); });
  68. Step 3 - Optimize performance • Before optimizing your application’s

    performance, you need to know about preflight request
  69. 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
  70. Request Flow 015*0/4 $034JOGPSNBUJPO ① Preflight Request (&5 3FTQPOTF

  71. Request Flow 015*0/4 $034JOGPSNBUJPO ② Request you want to send

    (&5 3FTQPOTF
  72. 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
  73. 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
  74. 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
  75. 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
  76. 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
  77. 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
  78. Then, how to optimize?

  79. –Sota Sugiura l/PUIJOHJTUIFCFTUBSDIJUFDUVSFGPS BQQMJDBUJPO`TQFSGPSNBODFz

  80. 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
  81. 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
  82. For CSRF • Adding origin checking on server • Using

    preflight request • The thing you should think about is not to exec request before any checking
  83. CSRF http://localhost:10000 http://localhost:8000 #BE3FRVFTU

  84. 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}`); });
  85. 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}`); });
  86. You need to use preflight? • In some case, you

    can’t avoid using preflight request • Don’t worry, you can optimize around preflight request
  87. Cache Preflight http://localhost:9000 http://localhost:8000 1SFGMJHIU const apiServer = 'http://localhost:8000'; fetch(apiServer)

    .then(res => { console.log(res); }) .catch(err => { // Do nothing });
  88. 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
  89. 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
  90. 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
  91. 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
  92. That’s all • ✅ Basic settings for CORS • ✅

    Ajax with credentials • ✅ Avoid using Preflight Request • ✅ Or cache request for optimizing
  93. 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
  94. Thank you