Day 11 : 싸우자 해커야! CSRF, HttpOnly, CSP 대응하기

2023. 6. 2. 15:50International Sign Lang 프로젝트/백엔드

728x90
 

Day 10 : MySQL2 오류, "Client does not support authentication protocol requested by server" 해결 방법

Day 9 : 에러나면 손모가지 날라가붕께 더보기 Day 8 : 가상 DOM 생성, 웹 스크래핑, 및 JSON 데이터 MySQL 저장 (후 많다 많아) Day 7 : 왜 굳이 웹 서버 프레임워크를 사용해야 하나? 2023.05.24 - [Project ISL/프

xpmxf4.tistory.com

위 글을 읽고 와주시면 감사하겠습니다!


1. 오늘의 주제

오늘의 주제는 뭘까요?

오늘의 주제는 바로 기존의 코드에 보안 이슈를 해결하는 것입니다!

먼저 저번까지 완료된 /countries API의 코드를 보겠습니다.

const express = require("express")
const mysql = require("mysql2")
const fs = require("fs")
const router = express.Router()

const [host, user, password, database] = fs.readFileSync("database_config.txt", "utf8").split("\n")

const db = mysql.createConnection({
    host,
    user,
    password,
    database,
})

router.post("", function (req, res) {
    const from_country = req.body.from_country
    const to_country = req.body.to_country

    if (from_country.length !== 2 || to_country.length !== 2) {
        return res.status(400).json({
            error: "Invalid country code",
        })
    }

    const query = "INSERT INTO isl (from_country, to_country) VALUES (?, ?)"
    db.query(query, [from_country, to_country], function (err, result) {
        if (err) {
            console.log(err)
            return res.status(500).json({
                error: "Database error",
            })
        }

        res.json({
            message: "Data Received",
            from_country: from_country,
            to_country: to_country,
        })
    })
})

module.exports = router

간단하게 FrontEnd 에서 from_country, to_country를 받아

DB 에다가 나라 클릭에 대한 결과를 넣는 API 입니다.

 

하지만 위 코드는 보안에 매우 허술한 코드입니다!

대표적으로 CSRF 공격, HttpOnly 쿠키, CSP 등의 보안 문제가 있죠!

 

이 문제들을 해결하지 않는 다면... 추후 제 사이트를 오픈했을 때

악성 공격에 대해 굉장히 무력한 모습을 보일 것입니다...ㅠ...

그렇기에 위 문제들에 대한 대비를 코드로 구현해 보도록 하겠습니다!

2. 주제에 대한 간략한 설명

이 글에서는 CSRF, HttpOnly, CSP에 대해 간단하게만 설명하고

위 사항들에 대해 구체적인 코드와 함께 다른 글로 찾아뵙도록 하겠습니다!

 

CSRF



[해석]


[출처] : 

HttpOnly



[해석]


[출처] : 

CSP



[해석]


[출처] : 

3. 코드로 실제로 구현해 보기

언제 Token을 발행해야 할까?

먼저 ISL 프로젝트의 특징 중 하나를 알아야 하는 게

ISL 은 현재 회원이라는 개념이 존재하지 않습니다.

보통은 CSRF 토큰은 로그인/회원가입 하면서 동시에 발행을 하는 데

저의 경우에는 로그인/회원가입을 하는 경우가 존재하지 않습니다.

그래서 언제 CSRF 토큰을 발행하냐에 대한 고민을 했어야 했습니다.

 

생각한 타이밍은 2 가지입니다.

  1. 프론트엔드에서 첫 POST 때 브라우저에 CSRF 토큰을 불러오고, 그 후에는 계속 토큰을 담아 보내기
  2. 웹페이지에 처음 접속했을 때, 프론트엔드에서 백엔드로 CSRF 토큰 요청하기

2 가지 중 2 번째 방 안으로 택하게 되었습니다.

 

ISL 특성상 사용자는 한 번만 클릭하기보단

여러 번을 클릭하게 될 것입니다.

그렇기에 처음 법규를 날리기(/countries, POST)를 했을 때

토큰을 불러오느라 웹페이지가 느리다는 경험을 줄 수 있습니다.

 

그래서 2번으로 택하게 되었습니다.

그래서 어떻게?

예전의 글들처럼, 일일이 CSRF 공격에 대해 대응하기 위해

순수하게 JS 코드로 구현을 할 수 있지만 이 또한 수많은 에러와 보안, 예외상황을

고려해야 하기 때문에 손수 짜는 건 공수가 오래 듭니다!

 

그래서 이러한 각 언어, 프레임워크 별로 주로 사용하는 라이브러리가 존재하고

Node.js 같은 경우 이를 위해 csurf라는 라이브러리를 사용합니다!

 

(csurf 로고)

(csurf 설명 인용)

 

이렇게 csurf를 적용하기 위해 app.js(서버 설정)의 코드를 수정하고,

'/csrf-token'라는 CSRF 토큰 발행을 위한 API를 생성했습니다.

파일의 위치는 /routes/csrfToken.js입니다!

// csrfToken.js
var express = require("express")
var router = express.Router()

router.get("/", function (req, res, next) {
    res.json({
        csrfToken: req.csrfToken(),
    })
})

module.exports = router

 

csurf의 특징으로는 HttpOnly에 대한 걱정을 동시에 해결한다는 것입니다!

res.cookie('your_cookie_name', 'your_cookie_value', { httpOnly: true });

이런 식으로 별도로 response의 쿠키에 대한 설정을 해줘야 하지만

알아서 해주게 됩니다!

 

그리고 CSP 문제를 해결하기 위해 helmet이라는 라이브러리를 설치하고

이를 또 서버 설정을 해야 하기 때문에 app.js를 수정해야 합니다!

// app.js
var express = require("express")
var path = require("path")
var cookieParser = require("cookie-parser")
var logger = require("morgan")
var csrf = require("csurf")
var helmet = require("helmet")

var countriesRouter = require("./routes/countries")
var csrfTokenRouter = require("./routes/csrfToken")

var app = express()

app.use(logger("dev"))
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
app.use(cookieParser())
app.use(helmet())
app.use(
    helmet.contentSecurityPolicy({
        directives: {
            defaultSrc: ["'self'"],
            scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
            objectSrc: ["'none'"],
            upgradeInsecureRequests: [],
        },
    })
)

// Middleware
app.use(csrf({ cookie: true }))

// routers
app.use("/countries", countriesRouter)
app.use("/csrf-token", csrfTokenRouter)

// catch 404 and forward to error handler
app.use(function (req, res, next) {
    res.status(404).json({ error: "Not Found" })
})

// error handler
app.use(function (err, req, res, next) {
    // set locals, only providing error in development
    var message = err.message
    var error = req.app.get("env") === "development" ? err : {}

    // return the error message as JSON
    res.status(err.status || 500)
    res.json({ error: message, details: error })
})



module.exports = app

TIL

하나의 API 에도 정말 여러 보안 이슈가 생길 수 있다는 것과,

언어와 프레임워크 별로 이를 다루는 라이브러리들에 대해서도 알게 되었습니다.

 

지인 중에 Spring Boot + Spring Security로 오늘 해결한 문제를 간단하게 해결하신 분과

얘기하다 보니, 제가 한 것보다 세팅을 훨씬 쉽게 하셨더라고요.

728x90