JavaScript 보안 가이드 1편 - 입력데이터 검증 및 표현

2023.01.27.

0. 시작하는 글

프로젝트를 하다가 고객사들은 보안 취약점에 대해 어떻게 대비를 하는지 조사를 하였습니다. Java, Python, C++ 언어들은 쉽게 정리된 자료를 검색할 수 있었는데 JavaScript는 서버에서는 Node.js 플랫폼에서 서버사이드 애플리케이션을 개발하는데 많이 사용되고 클라이언트에서도 웹페이지의 개발 작업에 두루 사용되는 인기 많은 언어임에도 불구하고 찾을 수가 없었습니다.
이에 JavaScript 보안 가이드의 정리가 필요하여 작업을 진행하였습니다. 한국인터넷진흥원에서 발간한 “Python_시큐어코딩_가이드”를 참조하여 작업하였고 필요한 텍스트와 이미지를 가져와 사용하였으며 내용을 더하고 소스는 만들어서 작성하였습니다.
문의사항이나 잘못된 부분을 발견하시면 아래 주소로 이메일 보내주시길 바랍니다.
-미디어나비 백엔드 개발자 퍼니 funny@medianavi.kr

1. SQL 삽입

가. 개요

image01
이미지출처 : 한국인터넷진흥원

SQL 삽입이란 데이터베이스(DB)와 연동된 웹 응용프로그램에서 입력된 데이터에 대한 유효성 검증을 하지 않을 경우 공격자가 입력 폼 및 URL 입력란에 SQL 문을 삽입하여 DB로부터 정보를 열람하거나 조작할 수 있는 보안취약점을 말한다. 취약한 웹 응용프로그램에서는 사용자로부터 입력된 값을 검증 없이 넘겨받아 동적쿼리(Dynamic Query)를 생성하기 때문에 개발자가 의도하지 않은 쿼리가 실행되어 정보유출에 악용될 수 있다.

나. 안전한 코딩기법

DB API 사용 시 인자화된 쿼리를 통해 외부 입력값을 바인딩해서 사용하면 SQL 삽입 공격으로부터 안전하게 보호할 수 있다. 이 방법을 SQL 파라미터화(parameterization) 방법이라고 한다. 이 방법을 사용하면, 사용자 입력 값이 SQL 쿼리로 실행될 때 별도의 파라미터로 전달되므로 악의적인 SQL 쿼리를 포함한 입력 값이 발생하더라도 실제 SQL 쿼리에 포함되지 않는다.

다. 코드 예제

SQL 삽입 문제가 있는 코드

var username = req.body.username;
var password = req.body.password;

var sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";

connection.query(sql, function(error, results) {
  if (error) {
    throw error;
  }
  res.json(results);
});

SQL 삽입(SQL Injection)이란, 사용자가 입력한 값이 악의적인 SQL 쿼리로 실행될 수 있는 것을 말한다. 위의 코드에서는 username과 password 값을 직접 SQL 쿼리에 포함시키고 있기 때문에, 악의적인 사용자가 입력한 값에 SQL 쿼리를 포함시키면 원하지 않는 결과가 발생할 수 있다.

아래의 코드처럼 SQL 파라미터화(parameterization) 방법을 사용하면 “SQL 삽입” 문제를 해결할 수 있다.

var username = req.body.username;
var password = req.body.password;

var sql = "SELECT * FROM users WHERE username = ? AND password = ?";

connection.query(sql, [username, password], function(error, results) {
  if (error) {
    throw error;
  }
  res.json(results);
});

SQL 파라미터화 방법을 사용하면, username과 password 값이 별도의 파라미터로 전달되므로 악의적인 SQL 쿼리를 포함한 입력 값이 발생하더라도 실제 SQL 쿼리에 포함되지 않는다.

또 아래 코드와 같은 방법도 있다.

var username = connection.escape(req.body.username);
var password = connection.escape(req.body.password);

var sql = "SELECT * FROM users WHERE username = " + username + " AND password = " + password;

connection.query(sql, function(error, results) {
  if (error) {
    throw error;
  }
  res.json(results);
});

위의 코드에서, connection.escape 메서드를 사용하여 username과 password 값을 이스케이프(escape)한다. 이스케이프된 값은 SQL 쿼리에 안전하게 포함될 수 있다.

2. 코드 삽입

가. 개요

image02
이미지출처 : 한국인터넷진흥원

공격자가 소프트웨어의 의도된 동작을 변경하도록 임의 코드를 삽입하여 소프트웨어가 비정상적으로 동작하도록 하는 보안 약점을 말한다. 코드 삽입은 프로그래밍 언어 자체의 기능에 한해 이뤄진다는 점에서 운영체제 명령어 삽입과 다르다.취약한 프로그램에서 사용자의 입력 값에 코드가 포함되는 것을 허용할 경우, 공격자는 개발자가 의도하지 않은 코드를 실행하여 권한을 탈취하거나 인증 우회, 시스템 명령어 실행 등을 할 수 있다.

나. 안전한 코딩기법

동적코드를 실행할 수 있는 함수를 사용하지 않는다. 필요 시, 실행 가능한 동적코드를 입력 값으로 받지 않도록 외부 입력값에 대하여 화이트리스트 방식으로 검증한다. 또는 유효한 문자만 포함하도록 동적 코드에 사용되는 사용자 입력 값을 필터링한다.

다. 코드 예제

외부 소스에서 악의적인 코드를 삽입하여 웹 사이트를 악용하는 문제를 방지하기 위해서는 외부 입력을 정제하는 과정을 거쳐야 한다. 예를 들어, 웹 폼에서 사용자가 입력한 값을 그대로 출력하는 경우 아래와 같은 코드를 사용할 수 있다.

<form>
  <textarea id="user-input"></textarea>
  <button onclick="displayInput()">Display Input</button>
</form>


<script>
  function displayInput() {
    var input = document.getElementById("user-input").value;
    document.write(input);
  }
</script>

하지만 이렇게 그대로 출력하면 사용자가 입력한 값에 스크립트가 포함되어 있을 경우, 그 스크립트가 실행되어 웹 사이트가 악용될 수 있다. 이를 방지하기 위해서는 입력값을 정제하는 과정이 필요하다.

<form>
  <textarea id="user-input"></textarea>
  <button onclick="displayInput()">Display Input</button>
</form>
<script>
  function displayInput() {
    var input = document.getElementById("user-input").value;
    var sanitizedInput = input.replace(/</g, "&lt;").replace(/>/g, "&gt;");
  document.write(sanitizedInput);
  }
</script>

위와 같이 입력값에 포함된 태그를 정제하는 과정을 거치면, 악의적인 코드 삽입을 방지할 수 있다. 또 다른 방법으로는 특정 태그를 제거하는 것 이외에도, 특정 태그만 허용하는 것, 또는 특정 태그를 안전하게 출력할 수 있도록 변환하는 것도 있다.

3. 경로 조작 및 자원 삽입

가. 개요

image03
이미지출처 : 한국인터넷진흥원

검증되지 않은 외부 입력값을 통해 파일 및 서버 등 시스템 자원(파일, 소켓의 포트 등)에 대한 접근 혹은 식별을 허용할 경우, 입력 값 조작을 통해 시스템이 보호하는 자원에 임의로 접근할 수 있는 보안약점이다. 경로조작 및 자원삽입 약점을 이용하여 공격자는 자원의 수정·삭제, 시스템 정보누출, 시스템 자원 간 충돌로 인한 서비스 장애 등을 유발시킬 수 있다. 즉, 경로 조작 및 자원 삽입을 통해서 공격자가 허용되지 않은 권한을 획득하여, 설정에 관계된 파일을 변경하거나 실행시킬 수 있다.

나. 안전한 코딩기법

외부로부터 받은 입력값을 자원(파일, 소켓의 포트 등)의 식별자로 사용하는 경우 적절한 검증을 거치도록 하거나, 사전에 정의된 리스트에 포함된 식별자만 사용하도록 해야 한다. 특히, 외부의 입력이 파일명인 경우에는 필터를 적용해 경로순회(directory traversal) 공격의 위험이 있는 문자( /, , .. 등)를 제거해야 한다.

다. 코드 예제

var userInput = "../../secret.txt";
var filePath = "./resources/" + userInput;
var content = fs.readFileSync(filePath, "utf-8");

위 코드에서 userInput 변수에 사용자가 입력한 값을 사용하여 파일 경로를 생성하고, fs.readFileSync 함수를 사용하여 파일을 읽어오고 있다. 하지만 이러한 방식은 악의적인 사용자가 userInput에 “../” 등의 경로 조작 구문을 입력하여 시스템의 다른 경로에 있는 민감한 파일을 읽어오는 것을 막지 못하기 때문에 보안 취약점이 될 수 있다. 이러한 보안 취약점을 방지하려면, 사용자 입력에 대한 검증이 필요하며, 특정 경로만 접근 가능하도록 제한할 수 있다.

var userInput = "secret.txt";
var allowedFiles = ["secret.txt", "data.txt", "info.txt"];

if (allowedFiles.indexOf(userInput) === -1) {
  throw new Error("Access Denied: Invalid File");
}

var filePath = "./resources/" + userInput;
var content = fs.readFileSync(filePath, "utf-8");

위 코드에서, 사용자 입력인 userInput을 허용된 파일 목록인 allowedFiles와 비교하여 허용된 파일인지 확인하고 있다. 허용되지 않은 파일인 경우, 예외를 발생시키고 액세스를 거부한다. 허용된 파일인 경우에만 경로를 생성하여 파일을 읽어온다. 이렇게 하면, 악의적인 사용자가 경로 조작 구문을 입력하여 다른 경로에 있는 민감한 파일에 접근하는 것을 막을 수 있다.

4. 크로스사이트 스크립트

가. 개요

image04
이미지출처 : 한국인터넷진흥원

크로스사이트 스크립트 공격(Cross-site scripting Attacks)은 웹사이트에 악성 코드를 삽입하는 공격 방법이다. 공격자는 대상 웹 응용 프로그램의 결함을 이용하여 악성코드(일반적으로 클라이언트 측 JavaScript 사용)를 사용자에게 보낸다. XSS 공격은 일반적으로 애플리케이션의 호스트 자체를 대상으로 하지 않고 애플리케이션의 사용자를 목표로 삼는다. XSS는 공격자가 웹 응용프로그램을 속여 사용자의 브라우저에서 실행할 수 있는 형식의 데이터를 보낼 때 발생한다. 일반적인 HTML과 공격자가 제공한 XSS코드의 조합 뿐만 아니라 악성코드 다운로드, 플러그인 또는 미디어 콘텐츠를 이용하기도 한다. 사용자가 양식에 입력한 데이터 또는 서버에서 클라이언트 소프트웨어(브라우저 또는 WebKit등)의 종료점(endpoint)으로 전달된 데이터가 적절한 검증 없이 사용자에게 표시되도록 허용하는 경우 발생한다.

나. 안전한 코딩기법

외부 입력값 또는 출력값에 스크립트가 삽입되지 못하도록 문자열 치환 함수를 사용하여 &<>*‘/()등을 치환하거나, html라이브러리의 escape()를 사용한다. HTML 태그를 허용하는 게시판에서는 허용되는 HTML 태그들을 화이트리스트로 만들어 해당 태그만 지원하도록 한다.

javascript에서 막는 방법

var escapeHtmlMap = {
    "&" : "&amp;",
    "<" : "&lt;",
    ">" : "&gt;",
    '"' : '&quot;',
    "'" : '&#39;',
    "/" : '&#x2F;'
};

String.prototype.escapeHTML = function() {
    return String(this).replace(/[&<>"'\/]/g, function(s) {
        return escapeHtmlMap[s];
    });
};

var text = "안녕하세요! <b>데브쿠아</b>입니다.";
text.escapeHTML();

다. 코드 예제

<script>
  var name = prompt("What is your name?");
  document.getElementById("welcome").innerHTML = "Welcome, " + name;
</script>

이 코드는 사용자에게 이름을 입력받아 환영 메시지를 표시한다. 하지만, 악의적인 사용자가 다음과 같은 스크립트를 입력할 경우

<script>alert('Hello')</script>

악의적인 스크립트가 실행될 수 있어 보안 문제가 발생한다.
이러한 XSS 문제를 해결하기 위한 가장 기본적인 방법은 사용자 입력을 적극적으로 검증하는 것이다. 악의적인 스크립트를 막기 위해서는 아래의 방법 중 하나를 사용해야 한다.

  • 인코딩: 사용자 입력에 포함된 스크립트를 인코딩하여 브라우저가 실행할 수 없도록 한다.
  • 디코딩: 사용자 입력이 화면에 표시될 때 디코딩하여 스크립트가 실행되지 않도록 한다.
  • 허용된 태그만 허용: 허용된 HTML 태그만 허용하도록 제한한다.

아래와 같이 사용자 입력을 적극적으로 검증하여 크로스사이트 스크립트 문제가 발생하지 않도록 한다.

// XSS prevention using encoding
function preventXSS(userInput) {
    return encodeURIComponent(userInput);
}

// XSS prevention using allowed tags only
function preventXSS(userInput) {
    var allowedTags = ['p', 'b', 'strong', 'em', 'u', 'i'];
    var userInputArray = userInput.split(" ");
    var result = "";
    for (var i = 0; i < userInputArray.length; i++) {
        if (allowedTags.indexOf(userInputArray[i]) !== -1) {
            result += "<" + userInputArray[i] + ">";
        } else {
            result += userInputArray[i];
        }
    }
    return result;
}

5. 운영체제 명령어 삽입

가. 개요

image05
이미지출처 : 한국인터넷진흥원

적절한 검증 절차를 거치지 않은 사용자 입력값이 운영체제 명령어의 일부 또는 전부로 구성되어 실행되는 경우, 의도하지 않은 시스템 명령어가 실행돼 부적절하게 권한이 변경되거나 시스템 동작 및 운영에 악영향을 미칠 수 있다. 명령어 라인의 파라미터나 스트림 입력 등 외부 입력을 사용해 시스템 명령어를 생성 하는 프로그램을 많이 찾아볼 수 있다. 이 경우 프로그램 외부로부터 받은 입력 문자열은 기본적으로 신뢰할 수 없기 때문에 적절한 처리를 해주지 않으면 공격으로 이어질 수 있다.

나. 안전한 코딩기법

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

다. 코드 예제

운영체제 명령어 삽입 문제는 사용자가 입력한 데이터를 그대로 운영체제 명령어로 실행할 경우 발생할 수 있는 보안 문제이다. 이 문제를 해결하는 방법으로는 사용자 입력 데이터를 정제하는 것이 중요하다.

function sanitizeInput(input) {
  return input.replace(/[^\w\s]/gi, '');
}
const userInput = 'rm -rf /';
const sanitizedInput = sanitizeInput(userInput);
console.log(sanitizedInput);  // rm rf

위 코드에서 sanitizeInput 함수는 사용자 입력 데이터에서 특수 문자를 제거하는 기능을 가진다. 이 함수를 통해 정제된 데이터는 운영체제 명령어로 실행되지 않는다.

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

가. 개요

image06
이미지출처 : 한국인터넷진흥원

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

나. 안전한 코딩기법

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

다. 코드예제

JavaScript로 개발시 위험한 형식 파일 업로드를 방지하는 방법 중 하나는 파일 확장자를 검사하는 것이다. 예를 들어, 허용하려는 확장자만 허용하는 경우 아래와 같은 코드를 사용할 수 있다.

var allowedExtensions = /(\.jpg|\.jpeg|\.png|\.gif)$/i;

var file = document.getElementById("file").files[0];
var fileName = file.name;
if(!allowedExtensions.exec(fileName)) {
    alert("Invalid file type. Please upload a jpeg, png or gif.");
}

또한, 이미지 파일만 업로드할 수 있도록 MIME 타입을 검사하는 것도 효과적이다.

var file = document.getElementById("file").files[0];
var fileType = file.type;
if (fileType !== "image/jpeg" && fileType !== "image/png" && fileType !== "image/gif") {
    alert("Invalid file type. Please upload a jpeg, png or gif.");
}

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

가. 개요

image07
이미지출처 : 한국인터넷진흥원

사용자가 입력하는 값을 외부 사이트 주소로 사용해 해당 사이트로 자동 접속하는 서버 프로그램은 피싱(Phishing) 공격에 노출되는 취약점을 가진다. 클라이언트에서 전송된 URL 주소로 연결하기 때문에 안전하다고 생각할 수 있으나, 공격자는 정상적인 폼 요청을 변조해 사용자가 위험한 URL로 접속할 수 있도록 공격할 수 있다.

나. 안전한 코딩기법

리디렉션을 허용하는 모든 URL을 서버 측 화이트리스트로 관리하고 사용자 입력 값을 리디렉션 할 URL이 존재하는지 검증해야 한다. 만약 사용자 입력 값이 화이트 리스트로 관리가 불가능하고 리디렉션 URL의 인자 값으로 사용되어야만 하는 경우는 모든 리디렉션에서 프로토콜과 host정보가 들어가지 않는 상대 URL(relative)을 사용해야 하고, 검증해야 한다. 또는 절대 URL(absoute URL)을 사용할 경우 리디렉션을 실행하기 전에 사용자 입력 URL이 ”https://myhompage.com/” 처럼 서비스하고 있는 URL로 시작하는지를 확인해야 한다.

다. 코드 예제

아래의 코드는 “신뢰되지 않는 URL 주소로 자동 접속 연결” 문제의 예시이다.

location.href = untrustedUrl;

이 코드에서 untrustedUrl은 신뢰할 수 없는 URL이 들어있는 변수이다. 코드가 실행되면, 사용자의 브라우저가 untrustedUrl에 포함된 URL로 자동적으로 리다이렉트 된다.
이는 보안 문제가 될 수 있다. 왜냐하면 공격자가 untrustedUrl을 악성 웹사이트로 설정할 수 있기 때문에 민감한 정보를 사기하거나 사용자의 장치에 맬웨어를 설치할 수 있다.
이 문제를 해결하려면, untrustedUrl을 리다이렉트하기 전에 검증하는 것이 중요하다. 이는 신뢰할 수 있는 URL 목록과 비교하거나, 적절한 형식을 가지는지 확인하기 위해 정규 표현식을 사용하여 검사하는 것으로 할 수 있다.

아래의 코드는 이 문제에 대한 해결을 보여준다.

// URL 검증 함수
function validateUrl(url) {
  // URL 형식을 검사하는 정규 표현식
  const pattern = /^(?:(?:https?|ftp):\/\/)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/\S*)?$/;

  return pattern.test(url);
}

// 신뢰되지 않는 URL을 검증하고 리다이렉트
const untrustedUrl = "example.com";
if (validateUrl(untrustedUrl)) {
  location.href = untrustedUrl;
} else {
  console.error("Invalid URL: " + untrustedUrl);
}

위의 코드에서 URL의 유효성을 검사하기 위해 validateUrl 함수가 정의되어 있다. validateUrl 함수는 주어진 URL을 검사하기 위해 정규 표현식(regular expression)을 사용한다. 정규 표현식은 URL이 적절한 형식을 가지고 있는지 검사한다. URL이 유효하다면, 코드에서 location.href 값을 URL로 설정하여 리다이렉트한다. 만약 URL이 유효하지 않다면, 코드에서 console.error 함수를 사용하여 오류 메시지를 표시한다. 이러한 방식으로 신뢰되지 않는 URL을 검증하고 리다이렉트하는 과정에서 보안 문제를 최소화할 수 있다.

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

가. 개요

image08
이미지출처 : 한국인터넷진흥원

XML 문서에는 DTD(DocumentTypeDefinition)를 포함할 수 있으며, DTD는 XML 엔티티(entitiy)를 정의한다. 부적절한 XML 외부개체 참조 보안약점은 서버에서 XML 외부엔티티를 처리할 수 있도록 설정된 경우에 발생할 수 있다. 취약한 XML parser가 외부 값을 참조하는 XML 값을 처리할 때, 공격자가 삽입한 공격 구문이 동작되어 서버 파일 접근, 불필요한 자원 사용, 인증 우회, 정보 노출 등이 발생할 수 있다.

나. 안전한 코딩기법

로컬 정적 DTD를 사용하도록 설정하고, 외부에서 전송된 XML 문서에 포함된 DTD를 완전하게 비활성화해야 한다. 비활성화를 할 수 없는 경우에는 외부 엔티티 및 외부 문서 유형 선언을 각 파서에 맞는 고유한 방식으로 비활성화 한다. 외부 라이브러리를 사용할 경우 기본적으로 외부 엔티티에 대한 구문 분석 기능을 제공하는지 확인하고 제공할 경우 해당 기능을 비활성화 할 수 있는 방법을 확인하여 외부 엔티티 구문 분석 기능을 비활성화 한다. 많이 사용하는 XML 파서의 한 종류인 lxml의 경우 외부 엔티티 구문 분석 옵션인 resolve_entities 옵션을 비활성화 하여야 한다. 또한 외부 문서를 조회할 때 네트워크 액세스를 방지하는 no_network 옵션이 활성화(True) 되어 있는지도 확인하여야 한다.

다. 코드예제

XML 외부 개체 참조를 방지하기 위해서 입력값을 검증한다.

function removeScriptTag(input) {
  return input.replace(/<script[^>]*>([\S\s]*?)<\/script>/img, "");
}

var input = "<p>This is a test <script>alert('Hello World!');</script></p>";
var output = removeScriptTag(input);
console.log(output); // "<p>This is a test </p>"

또한, XmlHttpRequest 또는 Fetch API를 사용해 XML 데이터를 요청할 때, same-origin policy를 준수하도록 하는 것도 효과적이다. 이로 인해, 클라이언트가 요청할 수 있는 XML 데이터는 클라이언트가 요청을 하는 웹페이지가 속한 도메인에서만 제공된다.

9. XML 삽입

가.개요

image09
이미지출처 : 한국인터넷진흥원

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

나. 안전한 코딩기법

XQuery 또는 XPath 쿼리에 사용되는 외부 입력데이터에 대하여 특수문자 및 쿼리 예약어를 필터링 하고 파라미터화(Parameterized)된 쿼리문을 지원하는 XQuery를 사용한다.

다. 코드예제

XML 삽입 방지 함수

function preventXMLInjection(str) {
    return str.replace(/[<>&"]/g, function (c) {
        switch (c) {
            case "<": return "&lt;";
            case ">": return "&gt;";
            case "&": return "&amp;";
            case "\"": return "&quot;";
        }
    });
}

사용 예

const userInput = "<script>alert('XML');</script>";
const safeInput = preventXMLInjection(userInput);
console.log(safeInput);
// Output: "&lt;script&gt;alert(&#39;XML&#39;);&lt;&#47;script&gt;"

위 코드는 사용자 입력을 받아 특수 문자를 HTML 엔티티로 변환하는 함수를 제공한다. 이렇게 하면 <, >, &, ” 문자가 입력될 경우 원래의 문자로 인식되지 않아 XML 삽입 공격을 방지할 수 있다.

10. LDAP 삽입

가. 개요

image10
이미지출처 : 한국인터넷진흥원

공격자가 외부 입력을 통해서 의도하지 않은 LDAP(LightweightDirectoryAccessProtocol) 명령어를 수행할 수 있다. 즉, 웹 응용프로그램이 사용자가 제공한 입력을 올바르게 처리하지 못하면 공격자가 LDAP 명령문의 구성을 바꿀 수 있다. 이로 인해 프로세스가 명령을 실행한 컴포넌트와 동일한 권한(Permission)을 가지고 동작하게 된다. 외부 입력값을 적절한 처리 없이 LDAP 쿼리문이나 결과의 일부로 사용하는 경우, LDAP 쿼리문이 실행될 때 공격자는 LDAP 쿼리문의 내용을 마음대로 변경할 수 있다

나. 안전한 코딩기법

다른 삽입 공격과 마찬가지로 LDAP 삽입에 대한 기본적인 방어 방법은 적절한 유효성 검사이다.
⦁ 올바른 인코딩(Encoding) 함수를 사용하여 모든 변수 이스케이프(Escape)
⦁ 화이트리스트 방식의 입력 값 유효성 검사
⦁ 사용자 비밀번호와 같은 민감한 정보가 포함된 필드 인덱싱
⦁ LDAP 바인딩 계정에 할당된 권한 최소화

다. 코드예제

LDAP 삽입 문제를 방지하기 위해서는 입력 데이터를 적절하게 검증하는 것이 중요하다. 다음 예시 코드는 LDAP 검색 쿼리를 생성할 때, 특수문자를 제거하는 검증 코드이다.

app.post("/search", (req, res) => {
  const searchString = req.body.searchString;
  // 특수문자를 제거하여 LDAP 삽입 공격을 방지
  const sanitizedSearchString = searchString.replace(/[^a-zA-Z0-9 ]/g, "");
  // LDAP 검색 쿼리 생성
  const ldapQuery = `(&(objectClass=person)(cn=*${sanitizedSearchString}*))`;
  // 이하 LDAP 검색 코드
});

또한, 권한 관리를 통해 검색 권한이 없는 사용자가 LDAP 검색을 시도하는 것을 방지 할 수 있다.

const authorizedUsers = ["admin", "manager"];
app.post("/search", (req, res) => {
    const user = req.session.user;
    // 권한이 없는 사용자는 접근을 막음
    if (!authorizedUsers.includes(user)) {
    res.status(403).send("Unauthorized access");
    return;
    }
    // 이하 검증코드
});

LDAP 삽입 공격을 방지하려면 입력데이터 검증과 권한관리를 적용하는 것이 중요하다.

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

가. 개요

image11
이미지출처 : 한국인터넷진흥원

특정 웹사이트에 대해서 사용자가 인지하지 못한 상황에서 사용자의 의도와는 무관하게 공격자가 의도한 행위(수정, 삭제, 등록 등)를 요청하게 하는 공격을 말한다. 웹 응용프로그램이 사용자로부터 받은 요청에 대해서 사용자가 의도한 대로 작성되고 전송된 것인지 확인하지 않는 경우 발생 가능하고 특히 해당 사용자가 관리자인 경우 사용자 권한관리, 게시물 삭제, 사용자 등록 등 관리자 권한으로만 수행 가능한 기능을 공격자의 의도대로 실행시킬 수 있게 된다. 공격자는 사용자가 인증한 세션이 특정 동작을 수행하여도 계속 유지되어 정상적인 요청과 비정상적인 요청을 구분하지 못 하는 점을 악용한다.

나. 안전한 코딩기법

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

다. 코드예제

아래는 서버측에서 생성한 CSRF 토큰을 클라이언트에서 사용하는 예시 코드이다.

// 서버측 코드
app.get("/transfer", (req, res) => {
  const csrfToken = generateCsrfToken(req.user); // 사용자 인증 정보를 이용해 토큰 생성
  res.render("transfer", { csrfToken }); // 클라이언트에게 토큰 전달
});

app.post("/transfer", (req, res) => {
  const csrfToken = req.body.csrfToken;
  if (!isValidCsrfToken(req.user, csrfToken)) { // 사용자 인증 정보와 토큰을 확인
    return res.status(403).send("Invalid CSRF token");
  }
  // 이체 처리 코드
});

// 클라이언트 코드
<form action="/transfer" method="POST">
  <input type="hidden" name="_csrf" value="{{csrfToken}}">
  <input type="text" name="amount">
  <input type="text" name="to">
  <input type="submit" value="Transfer">
</form>

위 코드는 서버측에서 생성한 CSRF 토큰을 클라이언트에서 사용하는 예시이다. 계속해서 서버는 csrfToken을 만들어서 클라이언트에 전달하고 클라이언트는 이를 이용해 서버에서 위조된 요청을 걸러낸다.

서버측 코드에서는 generateCsrfToken 함수를 이용해 사용자 인증 정보를 기반으로 CSRF 토큰을 생성하고, 이를 클라이언트에 전달한다. 그리고 클라이언트로부터 전달받은 토큰과 사용자 인증 정보를 기반으로 isValidCsrfToken 함수를 이용해 토큰의 유효성을 검사한다.

클라이언트 코드에서는 서버로부터 전달받은 CSRF 토큰을 히든 타입의 입력 필드로 추가해 전송한다. 이렇게 서버에서 생성한 CSRF 토큰을 클라이언트에서 사용하면서 토큰을 이용해 서버측에서 요청의 위조를 걸러낼 수 있다.

12. 서버사이드 요청 위조

가. 개요

image12
이미지출처 : 한국인터넷진흥원

적절한 검증절차를 거치지 않은 사용자 입력값을 서버간의 요청에 사용하여 악의적인 행위가 발생할 수 있는 보안약점이다. 외부에 노출된 웹 서버에 취약한 애플리케이션이 존재하는 경우 공격자는 URL 또는 요청문을 위조하여 접근통제를 우회하는 방식으로 비정상적인 동작을 유도하거나 신뢰된 네트워크에 있는 데이터를 획득할 수 있다.

나. 안전한 코딩기법

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

다. 코드예제

아래의 코드는 서버사이드 요청 위조 공격의 예시이다.

var xhr = new XMLHttpRequest();
xhr.open("GET", "http://localhost:8080/sensitive-data");
xhr.send();

위의 코드는 로컬 호스트의 8080 포트에 접근하는 요청을 보낸다. 하지만, 악의적인 사용자가 http://localhost:8080/sensitive-data 대신 http://malicious-server.com/steal-data 를 사용하여 공격할 수 있다. 이는 애플리케이션이 신뢰하는 로컬 호스트에서 제어되지 않은 데이터를 수집할 수 있는 공격이다.

서버 사이드 요청 위조를 방지하기 위해서는 요청을 인증하는 것이 중요하다.
애플리케이션은 요청이 예상된 도메인에서 제공되는 것만 신뢰하도록 구성할 수 있다. 아래의 코드는 이 문제를 해결하는 예시이다.

var allowedDomains = ['localhost:8080'];
var xhr = new XMLHttpRequest();
xhr.open("GET", "http://localhost:8080/sensitive-data");
xhr.onreadystatechange = function() {
  if (xhr.readyState === XMLHttpRequest.DONE) {
    var origin = new URL(xhr.responseURL).origin;
    if (!allowedDomains.includes(origin)) {
      throw new Error('Unexpected domain: ' + origin);
    }
  }
};
xhr.send();

위의 코드에서, allowedDomains 변수는 신뢰할 수 있는 도메인의 목록이다. xhr.responseURL은 실제로 수신된 응답의 URL을 나타낸다. 요청이 수신된 후, 응답의 URL의 출처(origin)이 허용된 도메인의 목록에 포함되어 있는지 확인한다. 응답의 URL이 허용된 도메인의 목록에 포함되어 있지 않은 경우, 애플리케이션은 예외를 발생시킨다.

13. HTTP 응답분할

가. 개요

image13
이미지출처 : 한국인터넷진흥원

HTTP 요청에 들어 있는 파라미터(Parameter)가 HTTP 응답헤더에 포함되어 사용자에게 다시 전달될 때, 입력값에 CR(CarriageReturn)이나 LF(LineFeed)와 같은 개행문자가 존재하면 HTTP 응답이 2개 이상으로 분리될 수 있다. 이 경우 공격자는 개행문자를 이용하여 첫 번째 응답을 종료 시키고, 두 번째 응답에 악의적인 코드를 주입하여 XSS 및 캐시훼손(CachePoisoning) 공격 등을 수행할 수 있다. HTTP 응답분할 공격으로부터 어플리케이션을 안전하게 지키려면 최신 버전의 라이브러리, 프레임 워크를 사용하고 외부 입력값에 대해서는 철저한 검증 작업을 수행해야 한다.

나. 안전한 코딩기법

요청 파라미터의 값을 HTTP 응답헤더(예를 들어, Set-Cookie 등)에 포함시킬 경우 CR(\r), LF(\n)와 같은 개행문자를 제거한다. 외부 입력값이 헤더, 쿠키, 로그등에 사용될 경우에는 항상 개행 문자를 검증하고 헤더에 사용되는 예약어 등은 가능하다면 화이트리스트로 제한해야 한다.

다. 코드예제

HTTP 응답 분할(HTTP Response Splitting)을 방지하기 위해서는 사용자 입력 데이터에서 CRLF(Carriage Return, Line Feed)문자를 제거하는 것이 중요하다.

다음 예시 코드는 사용자 입력 데이터에서 CRLF문자를 제거하는 검증 코드이다.

app.post("/submit", (req, res) => {
  const userInput = req.body.userInput;
  // CRLF 문자 제거
  const sanitizedInput = userInput.replace(/[\r\n]/g, "");
  // 이하 정상적인 처리 코드
});

또한, 라이브러리를 사용하여 입력 검증을 하는 것도 좋은 방법이다. 예를들면 ‘validator.js’라이브러리를 사용하면 유용하다.

const validator = require('validator');
app.post("/submit", (req, res) => {
  const userInput = req.body.userInput;
  // CRLF 문자 제거
  const sanitizedInput = validator.escape(userInput);
  // 이하 정상적인 처리 코드
});

14. 정수형 오버플로우

가. 개요

image14
이미지출처 : 한국인터넷진흥원

정수형 오버플로우는 정수형 크기가 고정된 상태에서 변수가 저장할 수 있는 범위를 넘어선 값을 저장하려 할 때 실제 저장되는 값이 의도치 않게 아주 작은 수 또는 음수가 되어 프로그램이 예기치 않게 동작하게 되는 취약점이다. 특히 반복문 제어, 메모리 할당, 메모리 복사 등을 위한 조건으로 사용자가 제공하는 입력값을 사용하고 그 과정에서 정수형 오버플로우가 발생하는 경우 보안상 문제를 유발할 수 있다.

나. 안전한 코딩기법

기본 자료형을 사용하지 않고 패키지에서 제공하는 데이터 타입을 사용할 경우 해당 패키지에서 제공하는 데이터 타입의 표현 방식과 최대 크기를 반드시 확인해야 한다. 기본적으로 64비트 길이의 정수형 변수를 사용하며, 변수가 표현할 수 없는 큰 크기의 숫자는 문자열 형식(object)으로 변환하는 기능을 제공한다. 하지만 64비트를 넘어서는 크기의 숫자는 제대로 처리하지 못한다. 따라서 변수에 값 할당 전에 반드시 변수의 최소 및 최대값을 확인하고, 범위를 넘어서는 값을 할당하지 않는지 테스트해야 한다.

다. 코드예제

정수형 오버플로우(integer overflow)를 방지하기 위해서는 입력 데이터를 적절하게 검증하는 것이 중요하다. 다음 예시 코드는 입력 데이터가 32비트 정수의 범위를 넘어서는 경우 에러를 발생시키는 검증 코드이다.

app.post("/submit", (req, res) => {
  const userInput = req.body.userInput;
  // 정수형 오버플로우 검증
  if (userInput > 2147483647 || userInput < -2147483648) {
    res.status(400).send("Integer overflow");
    return;
  }
  // 이하 정상적인 처리 코드
});

또한, 클라이언트와 서버 모두에서 정수형 오버플로우를 방지하는 것이 좋다. 예를 들어, javascript는 부동 소수점 숫자를 사용하며, 이는 정수형 오버플로우를 방지하는데 도움이 된다.

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

가. 개요

image15
이미지출처 : 한국인터넷진흥원

응용프로그램이 외부 입력값에 대한 신뢰를 전제로 보호메커니즘을 사용하는 경우 공격자가 입력값을 조작할 수 있다면 보호메커니즘을 우회할 수 있게 된다. 개발자들이 흔히 쿠키, 환경변수 또는 히든필드와 같은 입력값이 조작될 수 없다고 가정하지만 공격자는 다양한 방법을 통해 이러한 입력값들을 변경할 수 있고 조작된 내용은 탐지되지 않을 수 있다. 인증이나 인가와 같은 보안결정이 이런 입력값(쿠키,환경변수, 히든필드 등)에 기반을 두어 수행되는 경우 공격자는 이런 입력값을 조작하여 응용프로그램의 보안을 우회할 수 있으므로 충분한 암호화, 무결성 체크를 수행하고 이와 같은 메커니즘이 없는 경우엔 외부사용자에 의한 입력값을 신뢰해서는 안 된다.

나. 안전한 코딩기법

상태정보나 민감한 데이터 특히 사용자 세션정보와 같은 중요한 정보는 서버에 저장하고 보안확인 절차도 서버에서 실행한다. 보안설계 관점에서 신뢰할 수 없는 입력 값이 응용프로그램 내부로 들어올 수 있는 지점과 보안결정에 사용되는 입력 값을 식별하고 제공되는 입력 값에 의존할 필요가 없는 구조로 변경할 수 있는지 검토한다.

다. 코드예제

부적절한 입력값을 통해 보안 기능이 우회되는 문제를 방지하기 위해서는 입력 데이터를 적절하게 검증하는 것이 중요하다. 다음 예시 코드는 사용자가 입력한 암호가 적절한지 검증하는 코드이다.

const expectedPassword = "secret_password";

app.post("/grant_admin", (req, res) => {
  const userPassword = req.body.password;
  if (userPassword !== expectedPassword) {
    res.status(401).send("Unauthorized");
    return;
  }
  // 이하 관리자 권한 부여 코드
});

위 코드에서, 사용자가 요청한 패스워드(userPassword)가 예상하는 패스워드(expectedPassword)와 일치하지 않는다면, 401 Unauthorized 상태 코드와 “Unauthorized” 메시지를 보낸다. 이처럼 예상하지 않은 값이 제출될 경우 적절한 오류 메시지와 상태 코드를 보내는 것이 중요하다. 이를 통해 부적절한 입력에 대한 공격을 막을 수 있다.

또 다른 예시로 아래 코드는 사용자의 입력값에 대한 범위 제한을 걸어 잘못된 입력값으로 인한 악용을 막고 있다.

app.post("/submit", (req, res) => {
  const userInput = req.body.userInput;
  // 정수형 오버플로우 검증
  if (userInput > 100 || userInput < 0) {
    res.status(400).send("Invalid input value");
    return;
  }
  // 이하 정상적인 처리 코드
});

여기서, userInput이 0보다 작거나 100보다 크다면 400 Bad Request 상태 코드와 함께 “Invalid input value” 메시지를 보내 사용자에게 알려주고 처리를 중단한다.

16. 메모리 버퍼 오버플로우

가. 개요

메모리 버퍼 오버플로우 보안약점은 연속된 메모리 공간을 사용하는 프로그램에서 할당된 메모리의 범위를 넘어선 위치에 자료를 읽거나 쓰려고 할 때 발생한다. 메모리 버퍼 오버플로우는 프로그램의 오동작을 유발하거나, 악의적인 코드를 실행시킴으로써 공격자 프로그램을 통제할 수 있는 권한을 획득하게 한다.

나. 보안대책

프로그램 상에서 메모리 버퍼를 사용할 경우 적절한 버퍼의 크기를 설정하고, 설정된 범 위의 메모리 내에서 올바르게 읽거나 쓸 수 있게 통제하여야 한다. 특히, 문자열 저장 시 널(Null) 문자로 종료하지 않으면 의도하지 않은 결과를 가져오게 되므로 널(Null) 문자를 버퍼 범위 내에 삽입하여 널(Null) 문자로 종료되도록 해야 한다.

다. 코드예제

“메모리 버퍼 오버플로우” 문제를 해결하는 자바스크립트 코드 예시는 다음과 같다.

function copyArray(src, srcStart, dest, destStart, length) {
  if (srcStart + length > src.length || destStart + length > dest.length) {
    throw new Error("Buffer overflow");
  }
  for (let i = 0; i < length; i++) {
    dest[destStart + i] = src[srcStart + i];
  }
}

여기서, srcStart + length 가 src 배열의 길이보다 크거나, destStart + length 가 dest 배열의 길이보다 크다면 Error 객체를 생성하여 “Buffer overflow” 메시지를 보내 오버플로우가 발생했음을 알려준다. 이외에는 정상적인 배열 복사 처리를 한다.

17. 포맷 스트링 삽입

가. 개요

image16
이미지출처 : 한국인터넷진흥원

외부로부터 입력된 값을 검증하지 않고 입·출력 함수의 포맷 문자열로 그대로 사용하는 경우 발생할 수 있는 보안약점이다. 공격자는 포맷 문자열을 이용하여 취약한 프로세스를 공격하거나 메모리 내용을 읽거나 쓸 수 있다. 그 결과, 공격자는 취약한 프로세스의 권한을 취득하여 임의의 코드를 실행할 수 있다. 공격자는 포맷 문자열을 이용하여 내부 정보를 문자열로 만들 수 있으며, 이를 반환하는 경우 내부의 중요 정보가 유출될 수 있다.

나. 안전한 코딩기법

포맷 문자열을 사용하는 함수를 사용할 때는 사용자 입력 값을 직접적으로 포맷 문자열로 사용하거나 포맷 문자열 생성에 포함시키지 않아야 한다. 사용자에게 입력받은 데이터를 포맷 문자열로 사용하고자 하는 경우에는 서식지정자를 포함하지 않거나 내장함수 또는 내장변수 등이 포함되지 않도록 해야 한다.

다. 코드예제

포맷 스트링 삽입(format string injection)문제를 방지하기 위해서는 사용자 입력 데이터를 적절하게 검증하는 것이 중요하다. 다음 예시 코드는 사용자 입력 데이터를 안전하게 사용하도록 하는 검증 코드이다.

app.post("/submit", (req, res) => {
  const userInput = req.body.userInput;
  // 사용자 입력 검증
  if (!userInput.match(/^[\w\s]+$/)) {
    res.status(400).send("Invalid input");
    return;
  }
  // 이하 정상적인 처리 코드
  const query = `SELECT * FROM users WHERE name = '${userInput}'`;
});

또한, 라이브러리를 사용하여 입력 검증을 하는 것도 좋은 방법이다. 예를들면 ‘mysql’라이브러리를 사용하면 유용하다.

const mysql = require('mysql');
app.post("/submit", (req, res) => {
  const userInput = req.body.userInput;
  // 사용자 입력 검증
  const sanitizedInput = mysql.escape(userInput);
  const query = `SELECT * FROM users WHERE name = ${sanitizedInput}`;
  // 이하 정상적인 처리 코드
});

작성자 Funny