[Java 풀스택 개발자] 시큐어 코딩편: 개발자가 알아야 할 웹 보안

부트캠프 일지/멀티캠퍼스 TIL
2026.03.17

 

0. 들어가기 전에

최근 몸이 안 좋았다. 공부를 게을리하지는 않았지만 수업 진도를 따라가는 것만으로도 버거워서 따로 강의 일지를 적는 것도 부담이 되어 좀 쉬었다. 하지만 TIL만큼은 계속 이어나가고 싶기 때문에 힘내어 공부해보려고 한다. 이번 주는 바로 시큐어 코딩을 배웠다. 즐거운 수업이었던 것에 반해 아쉬운 이야기를 하나 할까 한다.

 

이번 강의에서 아쉬웠던 지점

이번 강의에서 아쉬웠던 것은, 바로 커리큘럼 구조로 인하여 아직 모르는 내용을 따라해야 했던 지점인 듯하다. 아직 백엔드 과정이 아닌데 백엔드의 보안 관련한 실습을 하려고 하니 모두에게 어렵지 않았을까 싶다. 백엔드에 대한 기초가 없는 상태에서(자바 기본 문법이나 프로그래밍만 알고 있기 때문에, 실질적으로 백엔드 기초는 없다고 봐야 한다.) 어려운 보안부터 배우려니 혼란스러웠을 것이다. 이 부분이 좀 아쉬웠다.

 

오늘의 TIL

오늘의 TIL은 시큐어 코딩이라는 이름에 맞춰 보안에 대해서 이야기할 것이다. 정확히는, 보안 방법 자체보다는 웹사이트에서 들어오는 공격에 대해서 나열하고, 이에 대한 방어를 가벼이 소개하고자 한다. 지피지기면 백전백승이라는 말이 있듯이, 공격을 알면 막아낼 수 있다. 특히 이번에 배우는 보안이라는 개념은 웹 사이트의 작동 이상으로 실전에서 중요한 부분이다.

 

 

1. 우리는 왜 보안에 신경 써야 할까?

우리는 몇 년에 한 번씩, 경우에 따라 몇 달에 한 번씩 각종 사이트에서 개인 정보 유출이 일어났다는 기사를 보곤 한다. 이런 개인정보 유출 때문에 한 기업에 대한 인식이 나빠지기도 한다. 이렇듯이 웹 사이트는 "개인 정보"를 다룬다. 회원 가입 시 사용자는 자신의 이름, 휴대폰 번호, 주소, 비밀번호, 좀 더 들어가면 신용카드의 정보 등을 저장하는 경우도 존재한다.

 

SNS 이후부터 더욱 개인정보는 중요해지고, 탈취될 가능성이 높아졌다.

 

그런데 개발자가 보안을 잘못 구현하면 어떻게 될까? 로그인만 되면 된다는 식으로 넘어가다 보면, 공격자는 그 틈을 노려서 사용자 정보를 탈취할 수 있다. 우리가 만든 웹사이트가 해커에게 열려 있는 셈이다. 사용자는 자신의 정보를 그 사이트에 맡긴 것이고, 그런데 그 정보가 유출된다면 그 책임은 결국 개발자에게도 돌아온다. 보안은 단순히 잘 되면 좋고가 아니라, 사용자에게 약속한 신뢰를 지키는 일이다. 그래서 우리는 보안에 신경 써야 한다.

 

또 하나, 보안 사고가 나면 기업 입장에서도 손해가 크다. 개인정보보호법 위반으로 과징금을 물거나, 고객 이탈, 브랜드 이미지 하락 같은 문제가 생긴다. 개발 단계에서 조금만 더 신경 쓰면 막을 수 있는 취약점이, 나중에 큰 사고로 이어지는 경우가 많다. 시큐어 코딩은 결국 사용자를 보호하는 일이자, 개발자가 만든 서비스를 지키는 일이라고 할 수 있다.

 

 

2. SQL Injection

SQL 인젝션(SQL Injection)이란 사용자 입력값을 검증하지 않았을 경우, SQL 쿼리문에 쿼리를 임의로 삽입하여 하는 공격을 의미한다. 주 목적으로는 DB(데이터베이스)에 존재하는 사용자 정보를 포함한 데이터들을 무단으로 탈취하는 것이다. 데이터베이스에는 사용자(사이트 회원)의 개인정보 또한 들어있기 때문에, 이를 통해서 간단히 사용자의 패스워드를 포함한 개인 정보를 탈취할 수 있다.

 

아들의 이름으로 SQL 인젝션을 하는 예제.

 

 

논리적 에러를 이용한 공격

가장 대표적인 공격 패턴이 바로 페이로드(Payload)다. ' or 1=1 -- 라는 문자열을 로그인 입력창이나 검색창에 넣는 방식이다. 왜 이게 먹히는지 코드로 보면 이해가 빠르다.

 


원래 서버가 의도한 쿼리는 이런 형태다.

String sql = "SELECT * FROM users WHERE id = '" + userId + "' AND password = '" + password + "'";

 

여기서 userId에 ' or 1=1 -- 를 넣으면 어떻게 될까?

 

SELECT * FROM users WHERE id = '' or 1=1 -- ' AND password = '아무거나'

 

1=1은 항상 참이기 때문에, WHERE 절 전체가 참이 되어 버린다. -- 뒤는 주석으로 처리되므로 password 검증도 무력화된다. 결국 users 테이블의 모든 레코드가 반환되는 결과가 나온다. 즉, 로그인 없이 전체 데이터를 탈취할 수 있게 된다.

 

이런 식으로 사용자 입력을 그대로 SQL 문자열에 붙여 넣는 방식이 취약하면, 공격자는 원하는 쿼리를 임의로 삽입할 수 있다.

 

 

union 삽입 공격

UNION은 두 개의 SELECT 결과를 합쳐서 하나로 보여주는 SQL 키워드다. 공격자는 이걸 이용해서 원래 쿼리 결과에 다른 테이블의 데이터를 함께 붙여서 가져온다.

예를 들어 원래 쿼리가 SELECT id, name FROM users WHERE id = '입력값' 이라면, 입력값에 ' UNION SELECT id, password FROM users -- 를 넣을 수 있다. 그러면 users 테이블의 id와 password가 함께 반환된다. 한 번에 많은 데이터를 탈취할 수 있는 방식이다.

 

 

오류 문구를 이용한 공격(Error Based SQL Injection)

SQL 인젝션 취약점이 있는 페이지에서, 특정 에러를 유발시키면 에러 메시지 안에 DB의 데이터가 그대로 노출되는 경우가 있다. 수업에서 사용한 Oracle의 경우 CTXSYS.DRITHSX.SN 같은 프로시저를 이용해, 에러를 발생시킬 때 서브쿼리 결과가 에러 메시지에 포함되도록 하는 방식이 있다. user_tables, all_tab_columns 같은 시스템 테이블을 이용해 테이블 구조를 파악한 뒤, 데이터를 추출하는 공격이다. 이런 식으로 catch 구문에서 예외를 그대로 출력하면 공격자가 에러 메시지를 통해 정보를 수집할 수 있다.

 

SQL 인젝션 공격에 대한 이미지

 

 

SQL Injection의 방어

사실 이 부분에 대해서는 지난 JDBC 편에서 다룬적이 있었다. PreparedStatement를 사용하는 방법이다.

 

[Java 풀스택 개발자] JDBC 편 : Java와 DB의 Connection, JDBC#4.-statement-vs-preparedstatement

 

[Java 풀스택 개발자] JDBC 편 : Java와 DB의 Connection, JDBC

0. 들어가기 전에 반성할 점요즘 스터디와 코테런 등등의 활동으로 TIL을 쓰는 것이 좀 소홀해졌다. 남들이 보기에는 충분히 훌륭하다고 할 수도 있겠지만, 내용을 정리하는 것에 급급한 느낌이

bbbbabbbababababa.tistory.com

 

PreparedStatement를 사용하여 플레이스홀더(?)로 바인딩하면, 사용자 입력이 SQL 명령으로 해석되지 않는다. 예를 들어 SELECT * FROM users WHERE id = ? AND password = ? 이런 식으로 쿼리를 작성하고, pstmt.setString(1, userId), pstmt.setString(2, password) 로 값을 넣으면, 사용자가 ' or 1=1 -- 를 넣어도 그냥 문자열로 취급되어 WHERE 조건이 깨지지 않는다. 입력값 필터링이나 화이트리스트 방식도 보조적으로 쓸 수 있지만, PreparedStatement가 가장 기본적인 방어라고 한다. 그 외에 에러 메시지에 DB 구조나 쿼리가 노출되지 않도록 catch 블록에서 예외를 그대로 출력하지 말고, 에러 페이지로 대체하는 것도 중요하다.

 

3. XSS (Cross-Site Scripting)

XSS는 웹사이트에서 파라미터 등을 받을 때, 사용자 입력이 그대로 출력되는 것을 악용하는 형태다. 주소창에 JS 코드를 넣거나 하여 악성코드를 심거나 할 수 있다. 게시글이나 댓글에도 사용자 입력이 그대로 출력되어 스크립트로 실행되기도 하기 때문에, 반드시 파라미터만 국한된다고 할 수 없다.

 

XSS 공격법

반사형 XSS

악의적인 사용자가 악성 스크립트를 넣은 URL을 만들어서 보낸다. 일반 사용자가 그 링크를 클릭하면, 서버가 그 URL의 파라미터를 그대로 응답 페이지에 넣어서 보내준다. 그러면 브라우저가 그걸 스크립트로 해석해서 실행한다. 서버는 거쳐가는 용도로만 쓰이기 때문에, 서버가 공격에 사용되는지 알 수 없다. 피싱의 시작점으로 많이 쓰인다고 한다.

<script>alert(document.cookie)</script>

이 문자열이 그대로 응답 HTML에 들어가면, 브라우저가 script 태그를 실행해서 쿠키를 알림창으로 띄운다. 공격자는 이걸 이용해 쿠키를 탈취하거나, 악성 사이트로 리다이렉트 시킬 수 있다.

 

 

저장형 XSS

반사형과 달리, 악성 스크립트가 서버(DB)에 저장된다. 그다음 그 페이지를 보는 모든 사용자에게 그 스크립트가 전달된다. 댓글, 게시글, 프로필 등에 스크립트를 넣어두면, 그걸 읽는 사람마다 악성 코드가 실행된다. DB에 저장된 script 태그는 서버에는 문제가 없지만, 클라이언트 브라우저에게 전달되는 순간 실행된다. 서버 관리자나 DB 관리자는 파악하기 어려울 수 있어서, 시큐어 코딩으로 막아야 한다. 악성 스크립트가 쿠키를 훔쳐서 공격자 서버로 전송하거나, 키로거를 설치하거나, DDoS 공격의 좀비로 쓰이게 만드는 등 피해 범위가 넓을 수 있다.

 

XSS 방어

서버에서 출력할 때 HTML/JS 특수문자를 이스케이프 처리한다. JSP라면 JSTL의 c:out 태그를 사용하면 된다. c:out value="${userInput}"은 <, >, &, " 같은 문자를 &lt; / &gt; / &amp; 같은 참조 문자로 바꿔서, 브라우저가 script 태그로 해석하지 못하게 한다. 입력값 필터링도 보조적으로 사용할 수 있지만, GET 방식으로 URL 파라미터를 넘기는 경우에는 클라이언트에서 막기 어렵기 때문에 서버에서 출력 시 이스케이프가 필수라고 한다. Content-Security-Policy(CSP) 헤더를 설정하면 인라인 스크립트 실행을 제한할 수 있어서, XSS 피해를 줄이는 데 도움이 된다.

 

 

4. CSRF (Cross-Site Request Forgery)

CSRF는 사이트 간 요청 위조라고 한다. 사용자가 로그인한 상태에서, 악의적인 사이트가 사용자의 브라우저를 통해 그 로그인된 사이트에 요청을 보내게 만드는 공격이다. 사용자는 자신이 요청을 보냈다고 생각하지 않지만, 악성 페이지에 이미지나 스크립트가 숨겨져 있어서 자동으로 로그인된 사이트에 POST 요청이 전송된다.

예를 들어 은행 사이트에 로그인한 상태에서, 공격자가 만든 페이지를 열었다고 하자. 그 페이지에 이런 코드가 있다.

 

<img src="https://은행사이트.com/transfer?to=공격자계좌&amount=1000000">

 

이미지가 로드되면서 GET 요청이 자동으로 전송된다. 사용자의 쿠키(세션)가 함께 전송되기 때문에, 서버 입장에서는 로그인된 사용자가 요청한 것처럼 보인다. 이렇게 사용자가 의도하지 않은 요청(이체, 비밀번호 변경 등)을 강제로 보내게 할 수 있다.

 

CSRF 공격

 

POST 요청의 경우에도 form을 숨겨두고 자동 제출하는 방식으로 공격할 수 있다. 사용자가 악성 페이지만 방문해도, 자바스크립트가 로그인된 사이트로 폼을 제출하게 만들 수 있다. XSS와 달리 CSRF는 사용자가 로그인된 사이트를 직접 조작하는 게 아니라, 사용자의 브라우저가 로그인된 상태의 쿠키를 이용해 요청을 보내게 만드는 점이 다르다. 그래서 공격자가 사용자의 쿠키를 알 필요는 없고, 사용자가 해당 사이트에 로그인만 되어 있으면 된다.

 

CSRF 방어

CSRF 토큰을 사용한다. 서버가 폼을 렌더링할 때마다 랜덤한 토큰을 hidden input에 넣어 보내고, 요청 시 그 토큰이 맞는지 검증한다. 공격자가 만든 페이지에서는 이 토큰 값을 알 수 없기 때문에, 요청이 거부된다. SameSite 쿠키 속성을 설정하면, 다른 사이트에서 요청이 들어올 때 쿠키가 전송되지 않아 CSRF 공격이 어려워진다. Referer 헤더를 검증하는 방법도 있지만, Referer가 비어 있거나 차단되는 경우가 있어서 토큰 방식이 더 안정적이라고 한다. Spring Security를 쓰면 CSRF 토큰 자동 생성·검증을 지원한다.

 

 

5. 취약한 접근 제어 (권한 우회)

접근 제어가 취약하면, 권한이 없는 사용자가 다른 사용자의 데이터나 관리자 기능에 접근할 수 있다. 사용자의 역할이나 권한에 따라 접근 가능한 데이터와 기능을 제한하지 못하는 경우가 여기 해당한다.

 

OWASP에서도 이 권한 우회가 제일 빈번히 일어난다고 적혀 있다.



대표적인 사례는 URL이나 쿼리 파라미터, 쿠키를 조작해서 권한 검증 없이 요청을 처리하게 만드는 것이다. 예를 들어 /mypage?userId=123 으로 접근하면 본인 정보가 보이는데, userId=456 으로 바꿔서 요청하면 다른 사용자 정보가 보인다면, 그건 권한 검증이 없다는 뜻이다. 서버에서 요청한 사용자와 userId가 일치하는지를 확인하지 않아서 생기는 문제다.

 

API 요청에서도 마찬가지다. POST, PUT, DELETE 같은 요청을 처리할 때 권한 확인 없이 처리하면, 공격자가 다른 사용자의 데이터를 수정하거나 삭제할 수 있다. 예를 들어 /api/users/456/delete 같은 엔드포인트가 있고, 로그인한 사용자 A가 userId만 456으로 바꿔서 요청하면 사용자 B의 계정이 삭제될 수 있다. IDOR(Insecure Direct Object Reference)라고 부르는 패턴이다. 서버는 매 요청마다 이 사용자가 해당 리소스에 접근할 권한이 있는지 검증해야 한다. Java에서는 Spring Security 같은 라이브러리를 활용하면 권한 검증 모듈을 체계적으로 추가할 수 있다고 한다.

 

방어법

모든 요청에 대해 서버에서 사용자 권한을 검증한다. URL 파라미터의 userId가 로그인한 사용자와 일치하는지, 해당 리소스에 접근할 권한이 있는지 검사해야 한다. Spring Security 같은 라이브러리를 쓰면 권한 검증 모듈을 체계적으로 추가할 수 있다. @PreAuthorize 같은 어노테이션으로 메서드 단위 권한을 설정할 수 있고, URL별로 역할(ROLE_USER, ROLE_ADMIN 등)을 매핑할 수 있다. IDOR를 막으려면 요청한 사용자와 리소스 소유자가 같은지 확인하는 로직이 꼭 필요하다.

 

 

6. 암호화 오류 / 민감정보 노출

암호화를 잘못 사용하거나, 민감정보를 노출하는 경우다. 개발자가 직접 암호 알고리즘을 만들거나, 하드코딩된 비밀번호를 쓰는 경우가 위험하다. 검증된 알고리즘을 사용해야 한다.

 

개인 정보에 대한 암호화에 대해서는 법령으로 이미 적혀 있다.

 

DES, MD5, SHA-1은 이미 취약점이 발견된 알고리즘이라 사용하지 않는 게 좋다고 한다. 비밀번호 해싱에는 SHA-2 계열과 salt 값을 함께 사용하는 것이 권장된다. salt는 예측 가능한 난수를 쓰면 안 된다. 같은 비밀번호라도 salt가 다르면 해시 결과가 달라지기 때문에, 레인보우 테이블 공격을 어렵게 만든다. 정부 고시에서도 선택된 알고리즘 사용, SHA-2와 salt 적용을 권장한다고 한다.

또 HTTP, FTP, SMTP 같은 암호화되지 않은 프로토콜로 민감정보를 전송하면 중간에 탈취될 수 있다. HTTPS를 사용하는 것이 기본이다. 서버 관리자 설정 영역이긴 하지만, 개발자도 평문 전송이 일어나는지 인지하고 있어야 한다.
민감정보가 에러 메시지나 로그에 그대로 출력되는 것도 문제다. catch 구문에서 예외를 그대로 출력하면, 스택 트레이스에 DB 비밀번호나 연결 정보가 나올 수 있다. 사용자 입력이 로그에 그대로 찍히면 공격자가 그걸 활용할 수 있다. 에러가 발생했을 때는 사용자에게는 일반적인 에러 페이지를 보여주고, 상세 내용은 로그에만 남기는 식으로 처리하는 게 좋다.

 

방어법

검증된 암호화 알고리즘을 사용한다. 비밀번호 해싱에는 bcrypt, argon2 같은 전용 알고리즘이 권장된다. salt는 예측 가능한 난수를 쓰지 말고, SecureRandom으로 생성한 값을 사용한다. HTTPS를 사용하면 전송 중 평문 노출을 방지할 수 있다. 에러 페이지와 로그에서 민감정보를 제거한다. catch 블록에서 e.printStackTrace()나 e.getMessage()를 그대로 출력하지 말고, 사용자에게는 일반적인 에러 메시지만 보여주고, 상세 내용은 서버 로그에만 남긴다. DB 연결 정보, API 키 같은 것도 환경 변수나 설정 파일로 분리하고, 코드에 하드코딩하지 않는 것이 좋다.

 

 

7. 마무리하며

이번에는 웹 보안에서 자주 등장하는 공격 유형들을 정리해보았다. SQL 인젝션, XSS, CSRF, 권한 우회, 암호화 오류 등은 OWASP TOP 10에도 포함되는 대표적인 취약점이다. 공격의 원리를 알아두면, 왜 PreparedStatement를 써야 하는지, 왜 입력값을 이스케이프해야 하는지가 자연스럽게 이해된다. 방어는 일단 막자가 아니라 어떤 공격을 막는지를 알고 그에 맞게 구현하는 것이 중요하다.

 

이미지 출처

※ 모든 이미지는 직접 제작하거나 저작권 문제가 없는 이미지로 제작되었습니다.

 

SNS 계정이 탈취되어 당황한 여성 - https://www.irasutoya.com/

SQL 인젝션에 대한 만화 - https://xkcd.com/327/

OWASP 웹사이트 - https://owasp.org/Top10/2025/A01_2025-Broken_Access_Control/

개인정보보호법 - https://www.law.go.kr/%EB%B2%95%EB%A0%B9/%EA%B0%9C%EC%9D%B8%EC%A0%95%EB%B3%B4%EB%B3%B4%ED%98%B8%EB%B2%95