JavaScript 시큐어코딩 가이드를 읽어보자 (제1절)

·2025년 7월 31일
post-thumbnail

이미지 출처 : Freepik

최근 사내에서 보안의 중요성을 강조하는 프로젝트의 제안이 있었고, 동료분께서 KISA 한국인터넷진흥원에 안내되어 있는 JavaScript 시큐어코딩 가이드를 공유해 주셨습니다.
이 가이드에서는 자바스크립트 개발 시 보안을 고려해야 하는 이유를 다음과 같이 설명합니다.

  • 작성한 코드의 많은 부분이 사용자에게 노출되고 사용자가 제공하고 요청하는 정보가 서버를 오가기까지 많은 신뢰할 수 없는 구간들을 거쳐야 하는 위험성을 가진다.
  • 지속 가능한 서비스 안정성 및 보안성 보장을 위해선 개발 단계부터 보안을 적용하는 것을 고려해야 한다. (NIST에서 공개한 자료에 따르면, 서비스 배포 이후 버그를 수정하는 비용이 설계 단계에서의 버그 수정에 비해 약 30배에 가까운 비용이 소요된다)

따라서 개발 과정에서 보안약점에 제거에 주의를 기울여야하며, 본 글에서는 1절에서 소개하는 보안 항목들을 정리하며, 각 항목에 대해 어떤 보안 위협이 있는지 그리고 어떻게 안전하게 코딩할 수 있는지를 예시와 함께 살펴보도록 하겠습니다.

🎬 본 가이드에서는 자바스크립트로 클라이언트, 서버 모두 개발이 가능한 점을 고려해 풀스택 상황에서의 보안 가이드를 제시하는 것을 목표로 합니다.


1. SQL 삽입

입력된 데이터에 대한 유효성 검증을 하지 않을 경우 공격자가 입력 폼 및 URL 입력란에 SQL 문을 삽입하여 DB로부터 정보를 열람하거나 조작할 수 있는 보안약점, 개발자가 의도하지 않은 쿼리가 실행되어 정보유출에 악용될 수 있다.

📌 안전한 코딩기법
사용자 입력값으로 쿼리를 생성하는 경우 쿼리 빌더를 사용해 SQL 인젝션 공격을 방어할 수 있다.

❌ 안전하지 않은 코드 예시
사용자 입력값을 받아 원시 쿼리를 구성해 처리 하는 안전하지 않은 코드 예시

const mysql = require("mysql");

// 커넥션 초기화 옵션은 생략함
const connection = mysql.createConnection(...);
                                          
router.get("/vuln/email", (req, res) => {
  const con = connection;
  const userInput = req.query.id;
  // 사용자로부터 입력받은 값을 검증 없이 그대로 쿼리에 사용
  const query = `SELECT email FROM user WHERE user_id = ${userInput}`;
  
  con.query(query,
    (err, result) => {
      if (err) console.log(err);
      return res.send(result);
    }
  );
});

🟢 안전한 코드 예시

const mysql = require("mysql");
...
router.get("/patched/email", (req, res) => {
  const con = connection;
  const userInput = req.query.id;
  const query =SELECT email FROM user WHERE user_id = ?;
  
  // 쿼리 함수에 사용자 입력값을 매개변수 형태로 전달, 이렇게 작성하면 사용자 입력값에 escape 처리를 한 것과 동일한 결과가 실행
  con.query(query, userInput,
    (err, result) => {
      if (err) console.log(err);
      return res.send(result);
    }
  );
});

2. 코드 삽입

공격자가 소프트웨어의 의도된 동작을 변경하도록 임의 코드를 삽입해 소프트웨어가 비정상적으로 동작하도록 하는 보안약점, 개발자가 의도하지 않은 코드를 실행해 권한을 탈취하거나 인증 우회, 시스템 명령어 실행 등으로 이어질 수 있다.

  • 공격을 유발할 수 있는 함수 : eval(), setTimeOut(), setInterval() 등…
  • setTimeOut(), setInterval() 는 문자열을 전달하면 eval로 실행하기 때문에 위험, 함수로 전달하면 안전

📌 안전한 코딩기법

  • 동적 코드를 실행할 수 있는 함수를 사용하지 않는다.
  • 필요 시 실행 가능한 동적 코드를 입력값으로 받지 않도록 외부 입력값에 대해 화이트리스트 기반 검증을 수행해야 한다.
  • 유효한 문자만 포함하도록 동적 코드에 사용되는 사용자 입력값을 필터링 하는 방법도 있다.
  • 데이터와 명령어를 분리해 처리하는 별도의 로직을 구현할 수도 있다.

❌ 안전하지 않은 코드 예시
외부로부터 입력 받은 값을 아무런 검증 없이 eval()을 사용해 사용자로부터 입력받은 값을 실행하여 결과를 반환 하는 예제

const express = require('express');

router.post("/vuln/server", (req, res) => {
  // 사용자로부터 전달 받은 값을 그대로 eval 함수의 인자로 전달
  const data = eval(req.body.data);
  return res.send({ data });
});

🟢 안전한 코드 예시
외부 입력값 내에 포함된 특수문자 등을 필터링 하는 사전 검증 코드를 추가하여 코드 삽입 공격 위험을 완화한다.

const express = require('express');

function alphanumeric(input_text) {
  // 정규표현식 기반 문자열 검사
  const letterNumber = /^[0-9a-zA-Z]+$/;
  
  if (input_text.match(letterNumber)) {
    return true;
  } else {
    return false;
  }
}

router.post("/patched/server", (req, res) => {
  let ret = null;
  const { data } = req.body;
  // 사용자 입력을 영문, 숫자로 제한하며, 만약 입력값 내에 특수문자가 포함되어
  // 있을 경우 에러 메시지를 반환
  if (alphanumeric(data)) {
    ret = eval(data);
  } else {
   ret = ‘error’;
  }
  return res.send({ ret });
});

3. 경로 조작 및 자원 삽입

검증되지 않은 외부 입력값을 통해 파일 및 서버 등 시스템 자원에 대한 접근 혹은 식별을 허용할 경우 입력값 조작으로 시스템이 보호하는 자원에 임의로 접근할 수 있는 보안약점, 허용되지 않은 권한을 획득해 자원 수정·삭제, 시스템 정보누출, 시스템 자원 간 충돌로 인한 서비스 장애 등을 유발시킬 수 있다.

📌 안전한 코딩기법

  • 외부로부터 받은 입력값을 자원(파일, 소켓의 포트 등)의 식별자로 사용하는 경우 적절한 검증을 거치도록 하거나 사전에 정의된 리스트에 포함된 식별자만 사용하도록 해야 한다.
  • 외부의 입력이 파일명인 경우에는 필터를 적용해 경로순회 공격의 위험이 있는 문자( /, \, .. 등)를 제거해야 한다.
    ❗경로순회(directory traversal) 공격 : 공격자가 파일 경로나 URL 등을 조작하여 접근해서는 민감한 정보에 접근하는 것 (e.g. 상대 경로 기호 (../)를 통해 파일에 접근)

❌ 안전하지 않은 코드 예시
외부 입력값을 검증 없이 소켓 연결 주소로 사용하는 예시

const express = require('express');
const io = require("socket.io");
router.get("/vuln/socket", (req, res) => {
  try {
  // 외부로부터 입력받은 검증되지 않은 주소를 이용하여
  // 소켓을 바인딩 하여 사용하고 있어 안전하지 않음
    const socket = io(req.query.url);
    return res.send(socket);
  } catch (err) {
    return res.send("[error] fail to connect");
  }
});

🟢 안전한 코드 예시
허용 가능한 목록을 설정한 후 목록 내에 포함된 주소만 연결을 허용하도록 코드를 작성하면 안전하게 만들 수 있다.

const express = require('express');
const io = require("socket.io");

router.get("/patched/socket", (req, res) => {
  // 화이트리스트 내에 속하는 주소만 허용
  const whitelist = ["ws://localhost", "ws://127.0.0.1"];
  if (whitelist.indexOf(req.query.url) < 0) {
    return res.send("wrong url");
  }
  try {
    const socket = io(req.query.url);
    return res.send(socket);
  } catch (err) {
    return res.send("[error] fail to connect");
  }
});

4. 크로스사이트 스크립트(XSS)

웹사이트에 악성코드를 삽입하는 공격 방법이다. 일반적으로 애플리케이션 호스트 자체보다 사용자를 목표로 하여 사용자가 폼 양식에 입력한 데이터 또는 서버에서 브라우저로 전달된 데이터가 적절한 검증 없이 사용자에게 표시되도록 허용되는 경우 발생한다.

XSS 공격 유형
1. Reflective XSS (or Non-persistent XSS)

  • 공격 코드를 사용자의 HTTP 요청에 삽입한 후 해당 공격 코드를 서버 응답 내용에 그대로 반사(Reflected)시켜 브라우저에서 실행하는 공격 기법이다.
  • 이 방법은 보통 악의적으로 제작된 링크를 사용자가 클릭하도록 유도하는 방식을 수반하여 공개 게시판, 피싱(Phishing) 이메일 등을 사용한다.

2. Persistent XSS (or Stored XSS)

  • 신뢰할 수 없거나 확인되지 않은 사용자 입력이 서버에 저장되고, 이 데이터가 다른 사용자들에게 전달될 때 발생한다.
  • 게시글 및 댓글 또는 방문자 로그 기능에서 발생할 수 있으며 공격자의 악성 콘텐츠를 다른 사용자들이 열람할 수 있다.

3. DOM XSS (or Client-Side XSS)

  • 웹 페이지에 있는 사용자 입력값을 적절하게 처리하기 위한 검증 로직을 무효화 하는 것을 목표로 한다.
  • Reflective XSS와 유사하다고 볼 수 있지만, 신뢰할 수 있는 사이트의 HTTP 응답에 페이로드를 포함하는 대신 DOM 또는 문서 개체 모델을 수정해 브라우저와 독립적인 공격을 실행한다는 점에서 차이가 있다.
  • 세션 및 개인 정보를 포함한 쿠키 데이터를 피해자의 컴퓨터에서 공격자 시스템으로 전송할 수 있다.

📌 안전한 코딩기법

  • 외부 입력, 출력값에 스크립트가 삽입되지 못하도록 문자열 치환 함수를 사용하여 Entity 코드로 치환하거나 라이브러리에서 제공하는 escape 기능을 사용해 문자열을 변환해야 한다.
  • encodeURI() 또는 encodeURIComponent() 함수를 사용하여 치환한다.
  • 허용할 HTML 태그들을 화이트리스트로 만들어 해당 태그만 지원하도록 한다.
  • 기본적으로 React DOM은 JSX 렌더링 전에 코드 내의 모든 값을 이스케이프 처리하지만, dangerouslySetInnerHTML 함수는 XSS 공격에 노출될 수 있기 때문에 주의하여 사용한다.

❌ 안전하지 않은 코드 예시
React에서 HTML 입력값을 그대로 렌더링할 수 있는 dangerouslySetInnerHTML 함수를 사용하는 예시

function possibleXSS() {
  return {
  __html:
    '<img
        src="https://upload.wikimedia.org/wikipedia/commons/a/a7/React-icon.svg“
        onload="alert(1)">
    </img>',
  };
}

const App = ( ) => (
  // XSS에 취약한 함수를 사용해 HTML 코드 데이터를 렌더링
  <div dangerouslySetInnerHTML={possibleXSS()} />
);
ReactDOM.render(<App />, document.getElementById("root"));

🟢 안전한 코드 예시
가급적이면 dangerouslySetInnerHTML 함수를 사용하지 않는 것이 좋겠지만, 부득이하게 사용이 필요한 경우 이스케이프 처리하는 컴포넌트를 별도로 개발하거나, dompurify와 같이 이스케이프 기능을 제공하는 라이브러리를 사용해 문자열 처리 후 사용한다.

<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.0/purify.min.js"></script>
...
function possibleXSS() {
  return {
    __html:
      // dompurify 라이브러리를 사용해 입력값을 이스케이프 처리
      DOMPurify.sanitize('<img src="https://upload.wikimedia.org/wikipedia/commons/
      a/a7/React-icon.svg"token function">alert(1)"></img>'),
    };
}
const App = ( ) => (
  <div dangerouslySetInnerHTML={possibleXSS()} />
);
ReactDOM.render(<App />, document.getElementById("root"));

5. 운영체제 명령어 삽입

사용자 입력값을 통해 시스템 명령어가 조작되면, ls, cat, rm 등의 명령이 실행되어 패스워드 조회, 프로세스 강제 종료 등 의도하지 않은 명령어 실행으로 이어질 수 있다.

📌 안전한 코딩기법

  • 외부 입력값에 시스템 명령어를 포함하는 경우 멀티라인 및 리다이렉트 문자(|, ;, &...) 등을 필터링하고 명령을 수행할 파일명과 옵션을 제한해 인자로만 사용될 있도록 해야 한다.
  • 외부 입력에 따라 명령어를 생성하거나 선택이 필요한 경우에는 필요한 값들을 미리 지정해 놓고 사용해야 한다.

❌ 안전하지 않은 코드 예시
사용자에게 특정 경로의 파일 목록을 제공하는 프로그램 예시, 만약 경로값 안에 파이프라인 명령어가 포함될 경우 악의적인 명령어가 실행될 수 있다. (e.g. cat /etc/passwd)

const express = require('express');
const child_process = require("child_process");

router.get("/vuln", (req, res) => {
  // 사용자가 입력한 명령어 인자값을 검증 없이 사용해 의도치 않은 추가 명령어 실행 가능
  child_process.exec("ls -l " + req.query.path, function (err, data) {
    return res.send(data);
  });
});

🟢 안전한 코드 예시
사용자가 입력한 값의 패턴을 검사해 허가되지 않은 패턴이 포함될 경우 기능을 실행하지 않거나 사용자 입력값 전체를 실행하고자 하는 명령어의 인자로 간주해 사용하는 방법이 있다.

const express = require('express');
const child_process = require("child_process");

router.get("/patched", (req, res) => {
  const inputPath = req.query.path;
  const regPath = /^(\/[\w^]+)+\/?$/;

  // 첫 번째 방법, 사용자 입력값 필터링 – 리눅스 경로 지정에 필요한 문자만 허용
  if (!inputPath.match(regPath)) {
    return res.send('not valid path');
  }

  // 두 번째 방법, 사용자 입력값이 명령어의 인자로만 사용되도록 하는 함수 사용
  child_process.execFile(
    "/bin/ls", ["-l", inputPath],
    function (err, data) {
      if (err) {
        return res.send('not valid path');
      } else {
        return res.send(data);
      }
    }
  );
});

6. 위험한 형식 파일 업로드

서버 측에서 실행 가능한 스크립트 파일(asp, jsp, php, sh 파일 등)이 업로드 가능하고 이 파일을 공격자가 웹을 통해 직접 실행시킬 수 있는 경우 시스템 내부 명령어를 실행하거나 외부와 연결해 시스템을 제어할 수 있는 보안약점이다.

📌 안전한 코딩기법

  • 특정 파일 유형만 허용하도록 화이트리스트 방식으로 파일 유형을 제한
  • 파일의 확장자 및 업로드 된 파일의 Content-Type도 함께 확인
  • 파일 크기 및 파일 개수를 제한하여 시스템 자원 고갈 등으로 서비스 거부 공격이 발생하지 않도록 제한
  • 업로드 된 파일을 웹 루트 폴더 외부에 저장해 공격자가 URL을 통해 파일을 실행할 수 없도록 제한
  • 업로드 된 파일의 이름은 공격자가 추측할 수 없는 무작위한 이름으로 변경 후 저장하는 것이 안전
  • 업로드 된 파일을 저장할 경우에는 최소 권한만 부여하고 실행 여부를 확인하여 실행 권한을 삭제하는 것이 안전

❌ 안전하지 않은 코드 예시
유효성 검사를 하지 않고 파일 시스템에 그대로 저장하는 예시

const express = require('express');

router.post("/vuln", (req, res) => {
  const file = req.files.products;
  const fileName = file.name;
  
  // 업로드 한 파일 타입 검증 부재로 악성 스크립트 파일 업로드 가능
  file.mv("/usr/app/temp/" + fileName, (err) => {
    if (err) return res.send(err);
    res.send("upload success");
  });
});

🟢 안전한 코드 예시
업로드 하는 파일의 개수, 크기, 파일 확장자 등을 검사해 업로드를 제한한다.
파일 타입 확인은 MIME 타입을 확인하는 과정으로 파일 이름에서 확장자만 검사할 경우 변조된 확장자를 통해 업로드 제한을 회피 할 수 있어 파일 자체의 시그니처를 확인하는 과정을 보여 준다.

const express = require('express');

router.post("/patched", (req, res) => {
  const allowedMimeTypes = ["image/png", "image/jpeg"];
  const allowedSize = 5242880;
  
  const file = req.files.products;
  const fileName = file.name;
  
  // 업로드 한 파일 타입 검증을 통해 악성 스크립트 파일 업로드 방지
  if (allowedMimeTypes.indexOf(file.mimetype) < 0) {
    res.send("file type not allowed");
  } else {
    file.mv("/usr/app/temp/" + fileName, (err) => {
    if (err) return res.send(err);
      res.send("upload success");
    });
  }
});

7. 신뢰되지 않은 URL주소로 자동접속 연결

사용자가 입력하는 값을 외부 사이트 주소로 사용해 접속하는 프로그램은 피싱공격에 노출되는 취약점을 가지며, 요청을 변조해 위험한 URL로 접속하도록 공격할 수 있다.

📌 안전한 코딩기법

  • 리다이렉션을 허용하는 모든 URL을 서버 측 화이트리스트로 관리하여 검증한다.
  • 화이트리스트로 관리가 불가능한 경우는 모든 리다이렉션에서 프로토콜과 host 정보가 들어가지 않는 상대 URL(relative URL)을 사용 및 검증한다.
  • 또는 절대 URL(absoute URL)을 사용할 경우 리다이렉션을 실행하기 전에 정상 서비스 중인 URL로 시작하는지 확인해야 한다.

❌ 안전하지 않은 코드 예시
검증 없이 redirect 함수의 인자로 사용해 의도하지 않은 사이트로 접근하도록 하거나 피싱공격에 노출되는 예시

const express = require('express');
router.get("/vuln", (req, res) => {
  const url = req.query.url;
  // 사용자가 전달한 URL 주소를 검증 없이 그대로 리다이렉트 처리
  res.redirect(url);
});

🟢 안전한 코드 예시
화이트리스트로 사전에 정의된 안전한 웹사이트에 한하여 리다이렉트 할 수 있도록 한다.

const express = require('express');

router.get("/patched", (req, res) => {
  const whitelist = ["http://safe-site.com", "https://www.example.com"];
  const url = req.query.url;
  // 화이트리스트에 포함된 주소가 아니라면 리다이렉트 없이 에러 반환
  if (whitelist.indexOf(url) < 0) {
    res.send("wrong url");
  } else {
    res.redirect(url);
  }
});

8. 부적절한 XML 외부 개체 참조

취약한 XML parser가 외부값을 참조하는 XML을 처리할 때 공격자가 삽입한 공격 구문이 동작되어 서버 파일 접근, 불필요한 자원 사용, 인증 우회, 정보 노출 등이 발생할 수 있다.

📌 안전한 코딩기법

  • 로컬 정적 DTD를 사용하도록 설정하고 외부에서 전송된 XML 문서에 포함된 DTD를 비활성화 해야한다.
  • 비활성화를 할 수 없는 경우에는, 외부 엔티티 및 외부 문서 유형 선언을 각 파서에 맞는 고유한 방식으로 비활성화 한다.
  • 라이브러리를 사용할 경우 기본적으로 외부 엔티티에 대한 구문 분석 기능을 제공하는지 확인하고, 해당 기능을 비활성화 한다.

❌ 안전하지 않은 코드 예시
XML 소스를 읽어와 분석하는 예시이다. 공격자는 XML 외부 엔티티를 참조하는 데이터를 전송하고 서버에서 해당 데이터 파싱 시 /etc/passwd 파일 내용이 사용자에게 전달될 수 있다.

// xxe.xml
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe1 SYSTEM "file:///etc/passwd" >
]>
<foo>&xxe1;</foo>
const express = require('express');
const libxmljs = require("libxmljs");

router.post("/vuln", (req, res) => {
  if (req.files.products && req.files.products.mimetype == "application/xml") {
    const products = libxmljs.parseXmlString(
      req.files.products.data.toString("utf8"),
      // 외부 엔티티 파싱 허용 설정(미설정 시 기본값은 false)
      { noent: true }
    );
    return res.send(products.get("//foo").text());
  }
  return res.send("fail");
});

🟢 안전한 코드 예시
libxmljs 라이브러리에서는 기본적으로 외부 엔티티 파싱 기능이 비활성화 되어 있지만 명시적으로 비활성 선언을 해주는 것이 좋다.

const express = require('express');
const libxmljs = require("libxmljs");

router.post("/patched", (req, res) => {
  if (req.files.products && req.files.products.mimetype == "application/xml") {
    const products = libxmljs.parseXmlString(
      req.files.products.data.toString("utf8"),
      // 외부 엔티티 파싱을 허용하지 않도록 설정
      // 미설정 시 기본값은 false이지만, 명시적으로 선언을 해 주는 것이 좋음
      { noent: false }
    );
    return res.send(products.get("//foo").text());
  }
  return res.send("fail");
});

9. XML 삽입

외부 입력값이 XQuery / XPath 쿼리문을 생성하는 문자열로 사용되어 공격자가 쿼리문의 구조를 임의로 변경하고 임의의 쿼리를 실행해 허가되지 않은 데이터를 열람하거나 인증절차를 우회할 수 있는 보안약점이다.

📌 안전한 코딩기법
XQuery / XPath 쿼리에 사용되는 외부 입력 데이터에 대하여 특수문자 및 쿼리 예약어를 필터링 하고 인자화된 쿼리문을 지원하는 XQuery를 사용해야 한다.

❌ 안전하지 않은 코드 예시
사용자 이름과 패스워드가 XML 내의 값과 일치할 경우 사용자 홈 디렉터리를 반환하는 예시로, 인자화된 쿼리문을 사용하지 않고 있어 공격자가 요청문을 조작해 패스워드 검증 로직을 우회할 수 있다.

const express = require('express');
const xpath = require('xpath');
const dom = require('xmldom').DOMParser;

const xml = `<users>
  <user>
    <login>john</login>
    <password>abracadabra</password>
    <home_dir>/home/john</home_dir>
  </user>
  <user>
    <login>cbc</login>
    <password>1mgr8</password>
    <home_dir>/home/cbc</home_dir>
  </user>
  </users>`;

router.get("/vuln", (req, res) => {
  const userName = req.query.userName;
  const userPass = req.query.userPass;
  
  const doc = new dom().parseFromString(xml);
  
  // 조작된 입력값(/vuln?userName=john' or ''='&userPass="' or ''='“)을 통해
  // 패스워드 검사 로직을 우회할 수 있음
  const badXPathExpr = "//users/user[login/text()='" + userName
                       + "' and password/text() = '" + userPass + "']/home_dir/text()";
  const selected = xpath.select(badXPathExpr, doc);
  
  try {
    const userPath = selected[0].toString();
    return res.send(`userPath = ${userPath}`);
  } catch {
    return res.send('not found');
  }
});

🟢 안전한 코드 예시
인자화된 쿼리를 통해 사용자가 조작한 입력값이 실행되지 않도록 한다.

const express = require('express');
const xpath = require('xpath');
const dom = require('xmldom').DOMParser;

const xml = `<users>
  ...
  </users>`;

router.get("/patched", (req, res) => {
  const userName = req.query.userName;
  const userPass = req.query.userPass;
  const doc = new dom().parseFromString(xml);
  
  // 인자화된 쿼리 생성
  const goodXPathExpr = xpath.parse("//users/user[login/text()=$userName and password/text()
  =$userPass]/home_dir/text()");
  // 쿼리문에 변수값 전달 및 XML 조회
  const selected = goodXPathExpr.select({
    node: doc,
    variables: { userName: userName, userPass: userPass }
  });
  try {
    const userPath = selected[0].toString();
    return res.send(`userPath = ${userPath}`);
  } catch {
    return res.send('not found');
  }
});

10. LDAP 삽입

외부 입력값을 LDAP 쿼리문이나 결과의 일부로 사용하는 경우, LDAP 쿼리문이 실행될 때 공격자는 LDAP 쿼리문의 내용을 마음대로 변경할 수 있다.

📌 안전한 코딩기법

  • 올바른 인코딩(Encoding) 함수를 사용해 모든 변수 이스케이프 처리
  • 화이트리스트 방식의 입력값 유효성 검사
  • 사용자 패스워드와 같은 민감한 정보가 포함된 필드 인덱싱
  • LDAP 바인딩 계정에 할당된 권한 최소화

❌ 안전하지 않은 코드 예시
사용자의 입력을 그대로 LDAP 질의문에 사용하는 예시

const express = require('express');
const ldap = require('ldapjs');

const config = {
  url: 'ldap://ldap.forumsys.com',
  base: 'dc=example,dc=com',
  dn: 'cn=read-only-admin,dc=example,dc=com',
  secret: 'd0accf0ac0dfb0d0fd...',
};

async function searchLDAP (search) {
...
}

router.get("/vuln", async (req, res) => {
  // 사용자의 입력을 그대로 LDAP 질의문으로 사용해 권한 상승 등의 공격에 노출
  const search = req.query.search;
  const result = await searchLDAP(search);
  
  return res.send(result);
});

🟢 안전한 코드 예시
LDAP 질의문에 사용될 변수를 이스케이프 하여 질의문 실행 시 공격에 노출되는 것을 예방 할 수 있다.

const express = require('express');
const ldap = require('ldapjs');
const parseFilter = require('ldapjs').parseFilter;

const config = {
  url: 'ldap://ldap.forumsys.com',
  base: 'dc=example,dc=com',
  dn: 'cn=read-only-admin,dc=example,dc=com',
  secret: 'd0accf0ac0dfb0d0fd...',
};

async function searchLDAP (search) {
...
}

router.get("/patched", async (req, res) => {
  let search;
  
  // 사용자의 입력에 필터링을 적용해 공격에 사용될 수 있는 문자 발견 시
  // 사용자에게 잘못된 요청값임을 알리고, 질의문을 요청하지 않음
  try {
    search = parseFilter(req.query.search);
  } catch {
    return res.send('잘못된 요청값입니다.');
  }

  const result = await searchLDAP(search);
  return res.send(result);
});

11. 크로스사이트 요청 위조(CSRF)

사용자의 의도와는 무관하게 공격자가 의도한 행위(수정,삭제, 등록 등)를 요청하게 하는 공격을 말한다. 요청이 사용자가 의도한대로 전송된 것인지 확인하지 않는 경우 발생 가능하다.

📌 안전한 코딩기법

  • 해당 요청이 정상적인 사용자가 절차에 따라 요청한 것인지 구분하기 위해 세션별로 CSRF 토큰을 생성하여 세션에 저장
  • 사용자가 작업 페이지를 요청할 때마다 hidden 값으로 클라이언트에게 토큰을 전달
  • 해당 클라이언트의 데이터 처리 요청 시 전달되는 CSRF 토큰값을 체크하여 요청의 유효성 검사

❌ 안전하지 않은 코드 예시
CSRF 토큰 처리가 없는 서버측 라우터 예시

const express = require('express');

router.post("/api/vuln", (req, res) => {
  const userName = req.body.username;
  const userEmail = req.body.useremail;
  
  // 사용자 업데이트 요청이 정상 사용자로부터 온 것이라고 간주하고,
  // 사용자로부터 받은 값을 그대로 내부 함수에 전달
  if (update_user(userName, userEmail)) {
    return res.send('update completed');
  } else {
    return res.send('update error');
  }
});

🟢 안전한 코드 예시
클라이언트측에서 렌더링을 처리하는 React의 경우 서버 기능 호출 전 서버로부터 토큰을 받아와 요청 헤더 내에 삽입해야 한다.

const App = () => {
  const getData = async () => {
    // 서버에 기능 호출 전 먼저 CSRF 토큰을 받아와 헤더에 저장
    const response = await axios.get('getCSRFToken');
    axios.defaults.headers.post['X-CSRF-Token'] = response.data.csrfToken;
  
    // CSRF 토큰이 설정된 상태에서 서버 기능 호출
    const res = await axios.post('api/patched', {
      username: ‘hello_user’,
      useremail: ‘test@email.com’,
    });
    document.write(res.data);
  };
  React.useEffect(() => { getData(); }, []);
  return <div>react-test</div>;
};
ReactDOM.render(<App />, document.getElementById("root"));

12. 서버사이드 요청 위조

사용자 입력값을 내부 서버간의 요청에 사용해 악의적인 행위가 발생할 수 있는 보안약점, 공격자는 URL 또는 요청문을 위조해 접근통제를 우회하는 방식으로 비정상적인 동작을 유도하거나 신뢰된 네트워크에 있는 데이터를 획득 할 수 있다.

📌 안전한 코딩기법

  • 식별 가능한 범위 내에서 사용자의 입력값을 다른 시스템의 서비스 호출에 사용하는 경우 사용자의 입력값을 화이트리스트 방식으로 필터링
  • 부득이하게 URL을 받아들여야 하는 경우라면 내부 URL을 블랙리스트로 지정하여 필터링
  • 동일한 내부 네트워크에 있더라도 기기 인증, 접근권한을 확인

❌ 안전하지 않은 코드 예시
사용자로부터 입력된 URL 주소를 검증 없이 사용하는 예시

const request = require('request');
const express = require('express');

router.get("/vuln", async (req, res) => {
  const url = req.query.url;
  
  // 사용자가 입력한 주소를 검증하지 않고 HTTP 요청을 보낸 후
  // 그 응답을 그대로 사용자에게 전달
  await request(url, (err, response) => {
    const resData = response.body;
    return res.send(resData);
  });
});

🟢 안전한 코드 예시
사전에 정의된 서버 목록에서 정의하고 매칭되는 URL만 사용할 수 있으므로 URL 값을 임의로 조작할 수 없다.

const request = require('request');
const express = require('express');

router.get("/patched", async (req, res) => {
  const url = req.query.url;
  const whiteList = [ 'www.example.com', 'www.safe.com'];

  // 사용자가 입력한 URL을 화이트리스트로 검증한 후 그 결과를 반환하여
  // 검증되지 않은 주소로 요청을 보내지 않도록 제한
  if (whiteList.includes(url)) {
    await request(url, (err, response) => {
      const resData = response.body;
      return res.send(resData);
    });
  } else {
    return res.send('잘못된 요청입니다');
  }
});

13. 보안기능 결정에 사용되는 부적절한 입력값

흔히 쿠키, 환경변수 또는 히든필드와 같은 입력값이 조작될 수 없다고 가정하지만 공격자는 다양한 방법을 통해 이러한 입력값들을 변경할 수 있다. 보안 결정이 이런 입력값에 기반을 두어 수행되는 경우 공격자는 입력값을 조작해 응용 프로그램의 보안을 우회할 수 있다.

📌 안전한 코딩기법

  • 상태 정보나 민감한 데이터 특히 사용자 세션 정보와 같은 중요 정보는 서버에 저장하고 보안확인 절차도 서버에서 실행
  • 인증 절차가 여러 단계인 경우 각 단계의 인증 절차가 진행될 때마다 서버에서는 각 단계별 인증값이 변조되지 않았는지 검증
  • 신뢰할 수 없는 입력값이 응용 프로그램 내부로 들어올 수 있는 지점을 검토하고, 민감한 보안 기능 결정에 사용되는 입력값을 식별해 입력값에 대한 의존성을 없애는 구조로 변경 가능한지 분석
  • 공격자가 데이터를 임의로 변경할 수 없도록 충분한 암호화, 무결성 체크를 수행, 메커니즘이 없는 경우 외부 사용자에 의한 입력값을 신뢰해서는 안됨

❌ 안전하지 않은 코드 예시
쿠키에 저장된 권한 등급을 가져와 관리자인지 확인 후에 사용자의 패스워드를 초기화 하고 메일을 보내는 예제

const express = require('express');

router.get("/admin", (req, res) => {
  // 쿠키에서 권한 정보를 가져옴
  const role = req.cookies.role;
  if (role === "admin") {
    // 쿠키에서 가져온 권한 정보로 관리자 페이지 접속 처리
    return res.render(‘admin’. { title: ’관리자 페이지‘ } ));
  } else {
    return res.send("사용 권한이 없습니다.");
  }
});

🟢 안전한 코드 예시
쿠키는 사용자가 자유롭게 수정할 수 있으므로, 중요 기능 수행을 결정하는 데이터는 위변조 가능성이 높은 쿠키보다 세션에 저장

const express = require('express');
const session = require("express-session");

app.use(session({
  secret: 'test',
  resave: true,
  saveUninitialized: true,
  store: new MemoryStore({checkPeriod: 60 * 60 * 1000})
}));

router.get("/patched", (req, res) => {
  // 세션에서 권한 정보를 가져옴
  const role = req.session.role;
  if (role === "admin") {
    // 세션에서 가져온 권한 정보로 관리자 페이지 접속 처리
    return res.render(‘admin’. { title: ’관리자 페이지‘ } ));
  } else {
    return res.send("사용 권한이 없습니다.");
  }
});

가이드가 160p 정도라서 세 번에 나눠서 정리해 볼 생각입니다.
정리하면서 지금 진행하고 있는 프로젝트에도 각각의 보안사항들이 잘 적용되어 있는지 확인해 보는 시간을 가질 수 있어서 좋았습니다 😁
본 가이드에 관심 있으신 분들은 한 번 읽어보셔도 좋을 것 같습니다!!

출처 : 과학기술정보통신부와 한국인터넷진흥원의 '자바스크립트 시큐어코딩 가이드'

profile
FE ✨

0개의 댓글