2. 입력 검증 취약점과 방지 방법

(1) XSS 방지

XSS 공격은 공격자가 악의적인 스크립트를 다른 사용자의 브라우저에서 실행하도록 하는 공격이다. 클라이언트 측에서 이러한 공격을 방지하려면 입력 데이터에 HTML 태그나 자바스크립트 코드가 포함되지 않도록 필터링해야 한다.

1) 취약점 코드 예시

다음은 사용자가 입력한 이름을 그대로 출력하는 기능이 있는 간단한 코드 예제이다. 해당 코드의 XSS 취약점과 방지하는 법에 대해 알아보자.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>XSS Vulnerable Page</title> </head> <body> <h1>Welcome</h1> <form method="GET"> <label for="name">Enter your name:</label> <input type="text" id="name" name="name"> <button type="submit">Submit</button> </form> <div> <h2>Hello, <!-- XSS 취약점이 있는 코드 --> <script> document.write(decodeURIComponent(location.search.split('=')[1])); </script> </h2> </div> </body> </html>
위 코드에서는 사용자가 입력한 값을 location.search에서 가져와서 document.write() 메서드를 이용해 페이지에 출력한다. 이 코드는 입력 값을 인코딩하거나 검증하지 않고 그대로 HTML에 삽입하기 때문에, 악성 스크립트를 포함한 입력 값이 실행될 수 있다. 예를 들어 공격자가 input[name=”name”]요소에 자바스크립트를 입력한다면 악성 스크립트를 실행시킬 수 있는 것이다.
위 코드에 대한 공격 시나리오는 아래와 같다.
page icon
공격 시나리오
  1. 사용자가 input[name=”name”] 에 아래와 같은 내용을 입력한다고 가정하자.
    1. <script>alert('XSS!');</script>
  1. 페이지는 입력된 name 값을 검증하지 않고 그대로 출력하므로, <script>alert('XSS!');</script> 스크립트가 실행된다.
  1. 결과적으로, 사용자의 브라우저에서 alert('XSS!')가 실행되며, 이로 인해 공격자가 원하면 더욱 악의적인 코드를 실행할 수 있다.

2) 코드 개선

XSS 공격을 방지하려면 입력 데이터를 그대로 출력하지 말고, HTML 엔티티로 인코딩하거나, 해당 입력 값을 검증해야 한다. HTML 엔티티로 인코딩하도 수정된 코드는 다음과 같다.
<!-- 중략... --> <div> <h2>Hello, <!-- XSS 방지를 위해 데이터를 안전하게 출력 --> <script> function sanitizeInput(str) { // 악성 스크립트 차단을 위해 특수 문자를 HTML 엔티티로 변환 // <, >, &, ", ' 등의 특수 문자가 HTML이나 자바스크립트로 해석되지 않음 return str.replace(/&/g, "&amp;") .replace(/</g, "&lt;") .replace(/>/g, "&gt;") .replace(/"/g, "&quot;") .replace(/'/g, "&#039;"); } const params = new URLSearchParams(window.location.search); const name = params.get('name'); if (name) { document.write(sanitizeInput(decodeURIComponent(name))); } </script> </h2> </div> <!-- 중략... -->
이제 사용자가 입력한 값에 악성 스크립트가 포함되어 있어도, 그것이 실행되지 않고 단순한 텍스트로 출력된다. 예를 들어, 공격자가 input 요소에 <script>alert('XSS!');</script>를 입력하더라도, 페이지에서는 <script>alert('XSS!');</script>를 그대로 출력할 뿐 실행하지 않는다.
 

(2) CSRF 방지

CSRF 공격은 사용자가 의도하지 않은 요청을 강제로 실행하게 만드는 공격이다. 이를 방지하기 위해 클라이언트 측에서 CSRF 토큰을 확인할 수 있다.

1) 취약점 코드 예시

아래의 간단한 코드 예시는 사용자가 로그인한 상태에서 비밀번호를 변경할 수 있는 페이지이다. CSRF 방어 조치가 없을 경우, 공격자는 사용자의 권한으로 악의적인 요청을 보낼 수 있다.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Change Password</title> </head> <body> <h1>Change Password</h1> <form id="passwordForm" method="POST" action="change_password.php"> <label for="newPassword">New Password:</label> <input type="password" id="newPassword" name="newPassword"><br> <button type="submit">Change Password</button> </form> </body> </html>
page icon
공격 시나리오
  1. 사용자가 이미 로그인된 상태에서 공격자가 조작한 악성 페이지를 방문한다.
  1. 공격자는 다음과 같은 URL로 사용자의 권한을 이용해 비밀번호 변경 요청을 전송한다.
    1. <img src="<http://example.com/change_password.php?newPassword=hacked123>" style="display:none;">
  1. 사용자가 이 페이지를 방문하면 이미지가 로드되면서 http://example.com/change_password.php?newPassword=hacked123로 요청이 자동으로 전송된다. 서버는 사용자가 요청한 것으로 착각하고 비밀번호를 변경하게 된다.

2) 코드 개선

① SSR의 경우(CSRF 토큰 방식을 통한 방어)

서버에서 생성된 CSRF 토큰을 폼에 추가하여 클라이언트가 요청을 보낼 때마다 서버는 이 토큰을 검증한다. CSRF 토큰은 세션마다 고유하며, 이를 통해 악성 사이트에서는 적절한 토큰을 포함한 요청을 보낼 수 없게 된다. CSRF 방어 시나리오는 다음과 같다.
page icon
CSRF 토큰 방식을 통한 방어
  1. 서버에서 토큰 발급: 사용자가 웹 애플리케이션을 처음 요청하면 서버는 고유한 CSRF 토큰을 생성한다.
  1. HTML에 토큰 삽입: 서버는 이 토큰을 HTML 페이지의 <form> 안에 숨겨진 필드로 추가하거나, 쿠키에 저장한다.
  1. 요청 시 토큰 전송: 사용자가 요청을 제출할 때 이 토큰이 서버로 함께 전송된다.
  1. 서버에서 토큰 검증: 서버는 요청에 포함된 CSRF 토큰이 유효한지 확인한 후, 요청을 처리한다. 토큰이 일치하지 않으면 요청을 거부하여 CSRF 공격을 차단한다.
<!-- 중략.. --> <!-- CSRF 토큰을 폼에 추가 --> <input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>"> <!-- 중략.. -->
위 코드에서는 HTML에 CSRF 코드를 삽입하고 있다. 이와 같이 클라이언트가 제출하는 모든 폼에는 CSRF 토큰이 포함되어야 한다. 이 토큰은 서버에서 생성된 토큰과 일치해야 하므로, 공격자가 임의로 조작한 요청은 토큰이 일치하지 않을 것이므로 실패하게 된다.

② CSR의 경우(토큰 기반 인증)

JWT(Json Web Token)와 같은 토큰 기반 인증 방식을 사용하여 요청할 때마다 헤더에 토큰을 포함시켜 사용자 인증과 함께 요청의 출처를 확인할 수 있다. 서버는 이 토큰이 클라이언트 측 애플리케이션에서만 생성되었는지 확인하고, 이 방식으로 CSRF 공격을 방지할 수 있다.
예시 (Axios로 API 호출 시 토큰 포함)
axios.post('/api/submit', { data: requestData }, { headers: { 'Authorization': `Bearer ${authToken}` } });
위와 같이 프론트엔드에서 서버로 요청할 때 Authorization 헤더에 토큰을 포함하여 전송한다. 이렇게 하면 공격자가 다른 웹사이트에서 직접적인 요청을 만들어도 이 헤더를 포함하지 못하기 때문에 공격을 차단할 수 있다.
 

(3) 파일 업로드 검증

사용자가 특정 파일을 업로드하려고 한다고 가정해 보자. 만약 사용자가 업로드한 파일이 서버에서 요구하는 파일 형식이 아니라면 어떤 문제가 일어날까? 예를 들어 .exe.js 와 같은 포맷의 악성 파일일 경우, 보안 문제가 발생할 수 있다. 따라서 클라이언트 측에서는 파일의 형식이 받고자 하는 형식이 맞는지, 안전한지 여부를 반드시 확인해야 한다.

1) 취약점 코드 예시

아래는 파일 업로드를 처리하는 간단한 HTML 폼과 JavaScript 코드이다. 클라이언트에서 파일 유형이나 크기에 대한 검증이 없으므로, 사용자가 악성 파일을 업로드할 수 있다.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>File Upload</title> </head> <body> <h1>Upload a File</h1> <form id="uploadForm" method="POST" action="upload.php" enctype="multipart/form-data"> <input type="file" name="file" id="file"><br> <button type="submit">Upload</button> </form> </body> </html>
page icon
공격 시나리오
  1. 공격자는 악성 스크립트가 포함된 .php, .js, .exe 등의 파일을 업로드할 수 있다.
  1. 예를 들어, shell.php와 같은 파일을 업로드하면 서버는 이 파일을 저장할 수 있고, 공격자는 해당 파일에 접근하여 서버에서 명령을 실행하거나 권한을 얻을 수 있다.
<?php // 악성 파일 예시 (shell.php) echo "This is a shell"; system($_GET['cmd']); ?>
공격자는 이 파일을 서버에 업로드한 후, URL을 통해 명령을 실행할 수 있다.
http://example.com/uploads/shell.php?cmd=ls
이로 인해 서버에서 임의의 명령이 실행될 수 있고, 심각한 보안 문제가 발생할 수 있다.

2) 코드 개선

파일 업로드는 클라이언트와 서버 간에 데이터를 주고받는 중요한 부분 중 하나이며, 이 과정에서 적절한 보안 조치가 취해지지 않으면 공격자가 악성 파일을 업로드하여 서버에 악영향을 끼칠 수 있다. 아래는 파일 확장자와 파일 크기를 클라이언트에서 미리 확인하고, 허용되지 않는 파일을 업로드하지 못하도록 하는 코드이다.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>File Upload</title> <script> function validateFile() { const fileInput = document.getElementById('file'); const file = fileInput.files[0]; // 허용되는 파일 확장자 const allowedExtensions = ['jpg', 'jpeg', 'png', 'gif']; const fileSizeLimit = 2 * 1024 * 1024; // 2MB 제한 const fileExtension = file.name.split('.').pop().toLowerCase(); // 파일 확장자 확인 if (!allowedExtensions.includes(fileExtension)) { alert('Only JPG, JPEG, PNG, and GIF files are allowed.'); return false; } // 파일 크기 확인 if (file.size > fileSizeLimit) { alert('File size must be less than 2MB.'); return false; } return true; } </script> </head> <body> <h1>Upload a File</h1> <form id="uploadForm" method="POST" action="upload.php" enctype="multipart/form-data" onsubmit="return validateFile();"> <input type="file" name="file" id="file"><br> <button type="submit">Upload</button> </form> </body> </html>
클라이언트 측에서는 파일의 유형과 크기, 확장자 등을 검사하는 기본적인 보안 조치를 적용할 수 있다. 하지만 클라이언트 측 검증은 사용자 경험 향상에 도움이 되지만, 신뢰할 수 없는 환경에서는 쉽게 우회될 수 있기 때문에 서버 측에서도 반드시 검증을 수행해야 한다.
 

(4) 입력 데이터 길이 제한

또 다른 예로, 사용자가 비밀번호를 입력한다고 가정해 보자. 만약 입력된 비밀번호가 데이터베이스에 저장할 수 있는 길이보다 길다면, 서버에 오류가 발생할 수 있다. 따라서 클라이언트에서 입력 데이터의 길이를 제한하여 이런 오류를 예방해야 한다.
클라이언트 측에서 입력 데이터의 길이를 제한하지 않으면, 버퍼 오버플로우(Buffer Overflow)나 서비스 거부 공격(Denial of Service, DoS) 등과 같은 다양한 공격에 노출될 수 있다. 특히 서버나 애플리케이션에서 이 입력을 그대로 처리할 경우, 성능 저하, 데이터 유실, 보안 취약점 발생 등의 문제가 생길 수 있다.
page icon
공격 시나리오
  • 버퍼 오버플로우(Buffer Overflow): 클라이언트에서 매우 긴 문자열을 입력하고 서버로 전송하면, 서버의 메모리의 특정 실행 권한이 있는 영역을 침범하여 해당 영역 공격자가 서버에 악의적인 코드를 주입하거나 예기치 않은 행동을 유도할 수 있다.
  • 서비스 거부 공격(Denial of Service, DoS): 클라이언트가 매우 큰 데이터를 여러 번 보내면서 서버의 자원을 소모시키면, 서버의 응답 시간이 느려지거나 완전히 중단될 수 있다.

1) 취약점 코드 예시

다음은 클라이언트 측에서 입력 데이터에 대한 길이 제한이 없는 간단한 폼이다. 이 폼은 서버에 데이터 전송을 허용하지만, 클라이언트에서 입력된 데이터의 길이를 전혀 검증하지 않는다.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Simple Form</title> </head> <body> <h1>Submit Your Feedback</h1> <form id="feedbackForm" method="POST" action="submit_feedback.php"> <label for="feedback">Feedback:</label><br> <textarea id="feedback" name="feedback"></textarea><br> <button type="submit">Submit</button> </form> </body> </html>
위 폼에는 길이 제한이 없으므로 사용자가 임의로 길이를 초과하는 데이터, 심지어 매우 큰 데이터를 입력할 수 있다. 이로 인해 서버는 많은 데이터를 처리해야 하며, 과도한 메모리 사용으로 인해 서버가 다운되거나 예기치 않은 동작을 유발할 수 있다.

2) 코드 개선

클라이언트 측에서 입력 데이터의 길이를 제한하는 것은 첫 번째 방어선에 해당한다. 하지만 이 검증은 쉽게 우회될 수 있기 때문에, 서버 측에서 반드시 추가적인 검증이 필요하다.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Feedback Form</title> <script> function validateInput() { const feedback = document.getElementById('feedback').value; const maxLength = 500; // 최대 글자 수 제한 if (feedback.length > maxLength) { alert(`Feedback cannot exceed ${maxLength} characters.`); return false; } return true; } </script> </head> <!-- 중략.. -->
 

(5) SQL 인젝션 방지

SQL 인젝션(SQL Injection)은 웹 애플리케이션에서 입력값을 제대로 검증하지 않아 발생하는 보안 취약점 중 하나로, 공격자가 악의적인 SQL 쿼리를 주입하여 데이터베이스를 조작하거나 민감한 정보를 탈취할 수 있게 만드는 공격이다. 이 공격은 사용자의 입력값이 직접적으로 SQL 쿼리에 삽입되는 경우 발생하며, 이를 통해 공격자는 데이터베이스의 구조를 파악하거나, 데이터를 조회, 수정, 삭제할 수 있다. 심지어, 시스템의 제어 권한을 획득할 수도 있다.
클라이언트 측에서 SQL 인젝션이 직접적으로 발생하지 않지만, 악성 입력을 필터하지 않고 서버로 전송할 경우 서버가 이를 제대로 처리하지 못하면 SQL 인젝션 공격을 당할 수 있다. 클라이언트 측에서 SQL 인젝션 공격을 방지하려면 입력 값을 적절하게 검증하고 클라이언트와 서버 간의 통신을 안전하게 유지하는 것이 중요하다.

1) 취약점 코드 예시

다음은 간단한 HTML과 JavaScript 코드로 구성된 클라이언트 측 폼이다. 이 코드는 사용자의 ID와 비밀번호를 입력 받아 서버에 전송한다. 서버가 이 입력 값을 검증하지 않으면 SQL 인젝션 공격을 받을 수 있다.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Login Page</title> </head> <body> <h1>Login</h1> <form id="loginForm" method="POST" action="login.php"> <label for="username">Username:</label> <input type="text" id="username" name="username"><br> <label for="password">Password:</label> <input type="password" id="password" name="password"><br> <button type="submit">Login</button> </form> </body> </html>
page icon
공격 시나리오
  1. 사용자가 ID와 비밀번호를 입력한 후 서버에 제출하면 서버에서 SQL 쿼리를 생성해 데이터베이스에 사용자 정보를 조회하는 형태로 동작한다.
    1. SELECT * FROM users WHERE userId = '입력된 ID' AND password = '입력된 비밀번호';
  1. 사용자가 username 필드에 정상적인 ID가 아닌 SQL 인젝션 공격 문자열을 입력할 수 있다. 예를 들어, 다음과 같은 값을 입력할 수 있다.
    1. ' OR 1=1 --
  1. 서버가 이 값을 처리할 때 SQL 쿼리¹⁾가 다음과 같이 조작될 수 있다.
    1. SELECT * FROM users WHERE username = '' OR 1=1 -- ' AND password = 'password';
  1. 이 쿼리는 항상 참이 되기 때문에, 공격자는 비밀번호를 입력하지 않고도 로그인 할 수 있고, 로그인을 우회하여 관리자 계정으로 접근해 관리자 권한을 얻거나 데이터베이스의 모든 사용자를 조회할 수 있다.

2) 코드 개선

JavaScript를 이용해 클라이언트 측에서 간단한 입력 검증을 수행할 수 있다. SQL 특수 문자나 비정상적인 입력을 차단하는 기본적인 검증을 적용할 수 있다.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Login Page</title> <script> // username과 password 필드에서 SQL 인젝션에 사용될 수 있는 특수 문자를 검사 function validateForm() { const username = document.getElementById('username').value; const password = document.getElementById('password').value; // 간단한 SQL 인젝션 패턴 필터링 //', ", ;, -을 포함한 문자열이 있는 경우, 사용자에게 경고를 표시하고 폼 제출을 중단 const sqlInjectionPattern = /['";--=()]/; if (sqlInjectionPattern.test(username) || sqlInjectionPattern.test(password)) { alert('Invalid characters in input.'); return false; // 폼 제출 중단 } return true; // 폼 제출 허용 } </script> </head> <body> <!-- 중략... --> </body> </html>
위와 같이 클라이언트 측에서 검증을 통해 쿼리에 특수 문자가 포함되지 않도록 할 수 있다. 하지만 클라이언트 측 입력 검증은 우회될 수 있기 때문에, 서버는 반드시 SQL 인젝션을 방어해야 한다. SQL 인젝션은 서버 측에서 반드시 막아야 하며 클라이언트 측에서만의 조치로는 막을 수는 없다.
 
지금까지 클라이언트 측에서의 입력 검증에 대해 알아보았다. 클라이언트 사이드 입력 검증으로 기본적인 보안 위협을 줄일 수 있지만, 이는 보조적인 방어책에 불과하다는 것을 꼭 기억하자. 보안 공격은 사용자가 자바스크립트를 비활성화하거나 클라이언트 검증을 우회하는 방법으로 시도될 수 있기 때문에, 모든 보안 검증은 서버 측에서도 반드시 이루어져야 한다.

¹⁾ 해당 쿼리에서 userId = '' 부분은 공백으로 무시되며 OR '1'='1' 조건이 항상 이 된다. --뒤는 주석으로 처리되므로 비밀번호 검증도 제대로 이루어지지 않는다. 즉, 쿼리 결과는 항상 참이 되는 것이다.