express-session의 secret은 도대체 뭘까요? (Express.js, Node.js)

0


express-session을 쓸 때 가장 찜찜한 부분은 아무래도 secret에 무엇을 넣을지에 대한 고민일 것 같습니다. 많은 자료들이 그냥 공식 문서에 나와 있듯이 'keyboard cat'인 채로 놔두고 이유를 설명하지 않지만, 대충 그런 식으로 아무 문자열이나 넣어도 되는 것 같지는 않아보였습니다.

  • secret 역할이 뭘까요?
  • 가장 권장되는 생성 방법이 뭘까요?
  • 얼마나 자주 교체해줘야 할까요?
  • 어떻게 교체해야 할까요? (secret rotation?)
  • 교체하면 무슨 일이 일어나나요?

이러한 여러 의문점들을 블로그 글을 작성하면서 해소해보겠습니다.

express-session의 공식 레퍼런스를 참고했습니다.
공식문서 바로 가기

그런데 공식 문서가 너무 빈약해서, SO 등 다양한 자료를 찾아봐야 했습니다.


공식 문서를 읽어도 풀리지 않는 의문들...

그러한 의문들에 대해 나름대로 스스로 답을 구해봤습니다.

secret의 역할이 뭘까요?

secret은 session ID cookie를 서명하는 데에 사용됩니다. 이를 통해 쿠키 변조를 막을 수 있습니다.

⚠️ 주의: 쿠키나 세션을 암호화하는 것이 아닙니다! 많은 자료들에서 세션을 암호화한다고 모호하게 설명하고 넘어가는데, 이는 잘못된 사실이라고 합니다! 서명과 암호화는 다른 개념이라고 합니다. 서명은 내용 유출을 막을 수 없고, 오로지 무결성만을 보장한다고 합니다.

쿠키 서명이 뭔가요?

https://stackoverflow.com/questions/45390058/what-is-the-core-functionality-of-the-secret-option-in-express-session

https://github.com/tj/node-cookie-signature

여기서 확인할 수 있습니다!

var cookie = require('cookie-signature');

var val = cookie.sign('hello', 'tobiiscool');
val.should.equal('hello.DGDUkGlIkCzPz+C0B064FNgHdEjox7ch8tOBGslZ5QI');

var val = cookie.sign('hello', 'tobiiscool');
cookie.unsign(val, 'tobiiscool').should.equal('hello');
cookie.unsign(val, 'luna').should.be.false;

위 코드를 보시면 아실 수 있듯이, 쿠키 서명은 굉장히 직관적이고 간단한 개념이라서, hellotobiiscool로 서명하면 hello.DGDUkGlIkCzPz+C0B064FNgHdEjox7ch8tOBGslZ5QI가 나옵니다. 그 값을 tobiiscool로 unsign하면 'hello'가 나옵니다. 따라서 유저가 hello.DGDUkGlIkCzPz+C0B064FNgHdEjox7ch8tOBGslZ5QI 값을 보내오면 서버가 숨겨진 secret tobiiscool값으로 서명을 해보고, 그 값이 . 앞의 값과 동일하면 쿠키가 변조되지 않았다는 사실을 확인할 수 있습니다.

공식 문서에서는 이러한 방식의 cookie signing이 cookie tampering을 막음으로써 session hijacking의 위험을 줄일 수 있다고 적혀 있습니다.

session hijacking이 뭔가요?

세션 탈취 공격은 공격자가 유저의 웹 세션을 가로채서 해당 사용자로서 시스템에 접근하는 방식의 공격입니다.

session hijacking을 막으려면 어떻게 해야 하나요?

  • HTTPS 사용
  • 세션 타임아웃
  • 세션 ID 예측 방지

등이 있습니다.

쿠키 서명이 어떻게 session hijacking을 막을 수 있나요?

쿠키 서명은 쿠키 데이터의 무결성을 보장합니다. 서버는 변조를 감지할 수 있습니다. 이러한 권한이 어떻게 세션 하이재킹을 방지할 수 있을까요? 직접적으로 방어할 수 있는 것은 아닐 듯 합니다.

https://www.reddit.com/r/webdev/comments/es5g5e/why_does_a_server_sign_a_cookie/

위 글에 따르면 다음 예시와 같은 방식으로 피해를 최소화 할 수 있다고 합니다.


200 OK
Set-Cookie: SessionID=7

이런 식으로 서버가 유저에게 응답하면

GET /MyAccountInfo
Set-Cookie: SessionID=7

유저는 다음에 서버에 위와 같은 http 요청을 보낼 수 있게 됩니다.

이때, 공격자가 나타나서 쿠키를 탈취하고, 내용을 참고하여 SessionID의 패턴을 예측하고, 새로운 쿠키를 만들어서 서버에 요청을 보내려고 시도합니다.

GET /MyAccountInfo
Set-Cookie: SessionID=8

서버는 꼼짝없이 위와 같은 공격에 당할 수 밖에 없습니다.

하지만 쿠키에 서명을 한다면 어떨까요?

200 OK
Set-Cookie: SessionID=7_8994e60788425e626e956576e4

이러면 공격자는 비록 SessionID가 7이라는 것을 알게 되어도, 쿠키의 내용을 변조할 수는 없어서 다른 유저에게 피해를 입힐 수 없게 됩니다.

이 방식의 한계는, 공격자가 여전히 SessionID가 7인 유저인 척 할 수는 있다는 점입니다. 그래서 완벽히 피해를 차단할 수는 없습니다.

하지만 쿠키 데이터가 전혀 변조되지 않았다는 사실을 안다는 것만으로도 얻을 수 있는 장점은 분명합니다!

secret은 어떻게 만드는 것이 가장 권장되나요?

secret은 랜덤하고 예측 불가능한 문자열이어야 합니다.
제 생각에는 아마 crypto 모듈을 사용해서 안전하게 생성할 수 있을 것 같습니다.

길이는 어느 정도가 권장될까요?

⚠️ 공식 문서에 이에 대해 세부적으로 적혀있지는 않습니다.

안전성을 보장하기 위해 충분한 길이가 필요하지만, 또 너무 길면 성능에도 영향을 줄 수 있고 그렇게 큰 도움이 되지는 않을 것 같습니다.

저와 비슷한 고민을 한 분들이 있습니다!
https://stackoverflow.com/questions/19732458/expressjs-ideal-session-secret-size

SO의 댓글에 따르면, express-session은 sha256을 서명에 사용하므로, 이를 위해 권장되는 최소의 키 크기인 256bit, 즉 32바이트의 길이면 충분하다고 하면서, 크립토 SO 답변을 근거로 들었습니다.

const crypto = require('crypto');
const secret = crypto.randomBytes(32).toString('hex');

위 코드를 통해 생성해서 사용하면 되겠네요! 물론 딱 한번만 실행하고, 해당 값이 git 같은 곳에 올라가지 않도록 환경 변수로 따로 빼서 process.env.SESSION_SECRET로 사용해야 합니다.

secret을 바꾸면 무슨 일이 일어나나요?

secret이 변경되면 기존의 secret으로 서명된 세션 쿠키는 무효화됩니다. 따라서 사용자들은 로그아웃 상태가 될 수 있으며, 그들은 다시 로그인 해야 합니다. 이런 이유로 secret을 교체하는 것은 사용자 경험에 영향을 줄 수 있기 때문에, 교체 작업 시 주의가 필요합니다.

얼마나 주기적으로 바꿔주는 것이 좋나요?

잘 모르겠습니다.

세션 데이터의 민감도에 따라 다르겠지만, secret을 바꾸면 그 이전 세션들이 모두 invalidate되니까, 너무 잦아도 안될 것 같아요!

https://github.com/expressjs/session/issues/591

이러한 재밌는 이슈 기록들과

https://stackoverflow.com/questions/5343131/what-is-the-sessions-secret-option

여기에서 Michael Mior라는 분이 불평하는 것으로 볼 때 예전에는 secret을 직접 수동으로 바꿔줘야 했지만, 1.11.0 부터는 secret rotation이라는 것을 지원해서 편해졌다네요!

express-session 1.11.0부터 지원하는 secret rotation이 뭔가요?

여러 개의 secret을 string의 배열 형태로 동시에 제공할 수 있게 함으로써 기존 세션을 무효화시키지 않고 안전하게 secret을 변경할 수 있는 방법을 제공합니다. secret 배열이 제공될 경우 첫 번째 요소는 새로운 세션을 서명하는 데에 사용되고, 나머지 요소들은 서명을 검증하는 데에만 사용됩니다. 이를 통해 최신 secret으로 새로운 세션을 생성하면서도 이전 secret으로 생성된 세션을 여전히 유효하게 유지할 수 있습니다.

그러면 secret의 배열들은 길이가 어느 정도인 게 좋나요?

잘 모르지만 길면 오버헤드가 심해지지 않을까요?

스프링에도 이와 비슷한 개념이 있을까요?

Spring Security에 비록 express-session과 비슷한 직접적인 cookie signing 메커니즘은 없지만, Remember-me와 같은 다른 방법들로 cookie의 무결성을 보장한다고 합니다.

결론

  1. secret은 쿠키 암호화를 위한 것이 아닙니다!

  2. 256비트짜리 문자열을 사용해봅시다!

  3. 최종 코드는 아래와 같은 형태를 띄겠네요.

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production', // use HTTPS in production
    maxAge: 60000 // example value, set based on your needs
  }
}));

저의 연구가 많은 도움이 되셨길 바랍니다!

0개의 댓글

관련 채용 정보