데이터베이스 보안 설계: SQL 인젝션 방지·최소 권한·암호화 적용


1장. 검색창은 고쳤는데 정렬 기능에서 다시 문제가 생겼다#

주문 조회 API에 검색 기능이 있습니다.

초기 코드는 사용자가 입력한 고객 ID를 SQL 문자열에 직접 붙였습니다.

보안 점검에서 문제가 발견되어 개발자는 매개변수 바인딩으로 수정했습니다.

이제 검색창은 안전해 보입니다.

그런데 얼마 뒤 새 기능이 추가됩니다.

사용자가 주문 목록을 다음 기준으로 정렬할 수 있게 했습니다.

최신순

금액순

주문번호순

개발자는 정렬 기준을 그대로 SQL 뒤에 붙였습니다.

검색 조건에서는 문자열 결합을 없앴지만 새로운 입력 경로가 SQL 구조에 다시 영향을 주게 됐습니다.

이 사례가 보여주는 핵심은 간단합니다.

데이터베이스 보안은 특정 공격 문자열 하나를 막는 작업이 아니라 입력이 SQL 구조를 결정하는 모든 경로를 통제하는 작업이다.


2장. 데이터베이스 보안은 하나의 방어선으로 끝나지 않는다#

보안을 다음 한 가지로 해결했다고 생각하기 쉽습니다.

SQL 인젝션 방지

하지만 실제 서비스에는 여러 보호 경계가 있습니다.

사용자 입력
↓
애플리케이션 권한 검사
↓
데이터베이스 계정 권한
↓
행·열 접근 범위
↓
전송 구간
↓
저장 장치
↓
백업
↓
로그
↓
테스트 환경

한 경계가 안전해도 다른 경계가 열려 있으면 데이터가 노출될 수 있습니다.


3장. 이번 글에서 사용할 주문 서비스#

다음과 같은 주문 서비스를 가정하겠습니다.

사용자:

고객 7

은 자신의 주문만 조회할 수 있습니다.

고객센터 직원은:

주문번호
고객번호
주문상태

만 볼 수 있습니다.

재무 보고 계정은:

일별 매출 집계

만 조회합니다.

웹 애플리케이션 계정은 주문 조회와 필요한 상태 변경만 수행합니다.

스키마 관리 계정은 배포 시에만 테이블 구조를 변경합니다.


4장. 필요한 권한과 주지 않을 권한부터 적어 보자#

주체 필요한 동작 주지 않을 동작
주문 조회 API 주문 조회·필요한 상태 변경 전체 스키마 변경
고객센터 제한된 주문 정보 조회 결제정보·민감정보 전체 조회
보고 계정 매출 집계 조회 고객 원본 테이블 조회
배포 계정 승인된 DDL 일상적인 웹 요청 처리
백업 계정 백업에 필요한 접근 애플리케이션 기능 수행

이 표가 최소 권한 설계의 출발점입니다.


5장. 최소 권한은 “읽기 전용 계정”보다 더 구체적이다#

보고 계정이 읽기만 가능하다고 하겠습니다.

그런데 다음 테이블을 모두 읽을 수 있습니다.

customer

orders

payment

employee

audit_log

쓰기 권한은 없지만 여전히 과도한 권한입니다.

최소 권한은:

SELECT 가능

인지 여부만 보는 것이 아니라:

어느 테이블을 읽는가?

어느 열을 읽는가?

어느 행을 읽는가?

어떤 함수와 뷰를 실행하는가?

까지 봐야 합니다.


6장. SQL 인젝션은 입력값이 SQL 구조로 해석될 때 발생한다#

취약한 코드를 개념적으로 보면 다음과 같습니다.

sql =
"SELECT order_id
 FROM orders
 WHERE customer_id = '" + input + "'"

정상 입력:

C100

이라면 의도한 조건이 만들어집니다.

하지만 입력에 SQL 문법으로 해석될 수 있는 문자가 들어오면 조건 구조가 바뀔 가능성이 생깁니다.

핵심 문제는:

사용자 입력
=
데이터 값

이어야 하는데 실제로는:

사용자 입력
=
SQL 문장의 일부

가 됐다는 점입니다.


7장. 공격 문자열을 외우는 것이 핵심이 아니다#

SQL 인젝션을 공부할 때 다음과 같은 예시가 자주 등장합니다.

' OR '1'='1

중요한 것은 이 문자열 자체를 차단 목록에 넣는 것이 아닙니다.

공격자는 다른 표현을 사용할 수 있습니다.

따라서 근본적인 방어는:

사용자 데이터를 SQL 구조와 분리하는 것

입니다.


8장. 매개변수 바인딩은 구조와 값을 분리한다#

Java의 PreparedStatement 예를 보겠습니다.

PreparedStatement query = connection.prepareStatement(
    "SELECT order_id, status " +
    "FROM orders " +
    "WHERE customer_id = ?"
);

query.setString(1, customerId);

여기에서 SQL 문장 구조는 이미 정해져 있습니다.

customerId는:

WHERE customer_id = ?

의 값으로 전달됩니다.

사용자 입력이 SQL 문법 자체를 다시 구성하지 않습니다.


9장. 따옴표가 들어가도 값으로 처리된다#

사용자가 다음 문자열을 넣었다고 하겠습니다.

O'Brien

문자열 결합에서는 따옴표 처리 문제가 발생할 수 있습니다.

매개변수 질의에서는 이 입력이:

문법

이 아니라:

문자열 값

으로 전달됩니다.

이 차이가 핵심입니다.


10장. 입력 검증은 매개변수 바인딩의 대체물이 아니다#

고객 ID 형식이:

C + 숫자 8자리

라고 하겠습니다.

애플리케이션에서 형식을 검사할 수 있습니다.

예:

C00001234

만 허용합니다.

이 검증은 잘못된 요청을 줄이는 데 유용합니다.

하지만:

형식 검증
=
SQL 인젝션 방어

라고 생각하면 안 됩니다.

형식 검증과 매개변수 바인딩은 역할이 다릅니다.


11장. 입력 검증은 업무 계약을 보호한다#

고객 ID 최대 길이가 20자인데 10MB짜리 입력을 보낸다고 하겠습니다.

매개변수 바인딩을 사용하더라도 불필요한 자원 사용이 발생할 수 있습니다.

따라서:

자료형

길이

허용 형식

허용 범위

를 검증하는 것이 좋습니다.

매개변수화는 SQL 구조를 보호하고 입력 검증은 서비스 계약과 자원 사용을 보호합니다.


12장. 정렬 열은 일반 값처럼 바인딩하기 어렵다#

다음 쿼리를 생각해 보겠습니다.

SELECT order_id, ordered_at, total_amount
FROM orders
WHERE customer_id = ?
ORDER BY ?;

두 번째 ?에:

ordered_at

을 넣는다고 해서 DBMS가 이를 항상 열 이름으로 해석하는 것은 아닙니다.

일반 바인딩 매개변수는 보통 값을 전달하는 용도입니다.

열 이름은 SQL 구조의 일부입니다.


13장. 동적 정렬에는 허용 목록을 사용하자#

화면에서는 다음 세 가지 정렬만 지원한다고 하겠습니다.

recent

amount

order

서버 코드에서 고정 SQL 표현으로 대응시킵니다.

개념적으로:

recent
→ ordered_at DESC

amount
→ total_amount DESC

order
→ order_id ASC

입니다.

사용자가 보내는 문자열을 직접 SQL 뒤에 붙이지 않습니다.


14장. 정렬 방향도 허용 목록으로 제한할 수 있다#

예를 들어:

asc

desc

만 허용합니다.

그 외 값은 거부합니다.

핵심은:

사용자가 SQL 조각을 보내는 구조

가 아니라:

사용자가 사전에 정의된 기능 중 하나를 선택하는 구조

로 만드는 것입니다.


15장. 테이블 이름도 같은 원칙을 적용한다#

사용자가:

table=orders

같은 값을 보내고 이를 SQL에 그대로 붙이는 구조는 신중해야 합니다.

실제 서비스에서는 보통 서버가 사용할 테이블을 알고 있습니다.

여러 데이터 집합 중 하나를 선택해야 한다면:

orders
→ fixed_query_1

returns
→ fixed_query_2

처럼 내부 매핑을 사용하는 편이 안전합니다.


16장. 저장 프로시저를 사용한다고 자동으로 안전해지는 것도 아니다#

다음과 같이 생각할 수 있습니다.

애플리케이션에서 SQL을 만들지 않고
저장 프로시저만 호출하면 안전하다.

하지만 프로시저 내부에서 다시:

사용자 문자열
+
SQL 문자열

을 결합해 동적 SQL을 만든다면 같은 문제가 생길 수 있습니다.

보안은 기술 이름이 아니라 실제 입력 흐름을 봐야 합니다.


17장. 매개변수 바인딩만으로 권한 문제가 해결되지는 않는다#

고객 7이 로그인했습니다.

API:

GET /orders?customer_id=8

을 호출했다고 하겠습니다.

SQL은 완벽하게 매개변수화되어 있습니다.

SELECT order_id, status
FROM orders
WHERE customer_id = ?;

문법 공격은 없습니다.

하지만 애플리케이션이 요청받은 customer_id=8을 그대로 신뢰한다면 고객 7이 고객 8의 주문을 볼 수 있습니다.

이것은 SQL 인젝션이 아니라 접근 제어 실패입니다.


18장. “읽을 수 있음”과 “읽어도 됨”은 다르다#

데이터베이스에서 다음 쿼리가 정상 실행된다고 하겠습니다.

SELECT *
FROM orders
WHERE order_id = 1001;

이것은 기술적으로 읽을 수 있다는 뜻입니다.

하지만 현재 로그인 사용자가 그 주문을 읽을 권한이 있는지는 별도 문제입니다.

쿼리 실행 가능
≠
업무상 조회 허용

입니다.


19장. 인증된 사용자와 데이터 소유자를 연결해야 한다#

주문 조회 요청에서는 클라이언트가 보내는 고객 ID보다 인증 정보가 기준이 되어야 합니다.

개념적으로:

로그인 사용자
customer_id = 7

이라면 서버가:

SELECT
    order_id,
    status,
    total_amount
FROM orders
WHERE order_id = ?
  AND customer_id = ?;

형태로 주문 소유권까지 확인할 수 있습니다.


20장. 다른 고객 주문 조회를 실제 실패 테스트로 만들어야 한다#

보안 테스트는 정상 요청만 성공하는지 확인하면 부족합니다.

허용#

고객 7
→ 고객 7 주문 조회
→ 성공

거부#

고객 7
→ 고객 8 주문 조회
→ 거부

두 결과가 모두 테스트되어야 합니다.


21장. RBAC은 사용자마다 직접 권한을 관리하는 복잡도를 줄인다#

사용자가 10,000명이라고 하겠습니다.

모든 사용자에게 개별적으로 권한을 부여하면 관리가 복잡합니다.

RBAC에서는 역할을 만듭니다.

support_reader

report_reader

order_writer

schema_admin

사용자는 필요한 역할을 부여받습니다.


22장. PostgreSQL 역할 예제를 보면#

CREATE ROLE report_reader NOLOGIN;

GRANT USAGE
ON SCHEMA analytics
TO report_reader;

GRANT SELECT
ON analytics.daily_sales
TO report_reader;

그 뒤 실제 로그인 계정에 역할을 부여할 수 있습니다.

GRANT report_reader
TO report_user;

핵심은 report_user에게 필요한 분석 객체만 접근하도록 만드는 것입니다.


23장. DAC·RBAC·MAC을 구분해 보자#

DAC#

Discretionary Access Control입니다.

객체 소유자 등이 권한을 부여하는 방식으로 설명할 수 있습니다.

RBAC#

Role-Based Access Control입니다.

권한을 역할에 묶고 사용자를 역할에 연결합니다.

MAC#

Mandatory Access Control입니다.

보안 레이블이나 중앙 정책에 따라 접근을 강제하는 모델입니다.

실제 시스템에서는 여러 개념이 섞여 사용될 수 있습니다.


24장. GRANT를 썼다고 시스템 전체가 순수 DAC인 것은 아니다#

PostgreSQL에서:

GRANT SELECT ON orders TO report_user;

를 사용했다고 하겠습니다.

이 명령 하나만 보고 전체 서비스 보안 모델을:

DAC 시스템

이라고 단정하기는 어렵습니다.

애플리케이션에는 RBAC이 있을 수 있고 데이터베이스에서는 행 수준 정책이 추가될 수 있습니다.

실제 보안 구조는 여러 계층의 조합입니다.


25장. 고객센터에는 원본 테이블 대신 제한된 뷰를 제공할 수 있다#

원본 주문 테이블에 다음 열이 있다고 하겠습니다.

order_id

customer_id

customer_phone

payment_token

delivery_address

status

total_amount

고객센터에는 모든 열이 필요하지 않습니다.

필요한 정보:

order_id

customer_id

status

뿐이라고 하겠습니다.


26장. 고객센터용 뷰를 만들 수 있다#

CREATE VIEW support_order AS
SELECT
    order_id,
    customer_id,
    status
FROM orders;

이제 고객센터 역할에는 뷰만 조회하도록 권한을 줄 수 있습니다.

GRANT SELECT
ON support_order
TO support_reader;

27장. 원본 테이블 접근은 제거해야 한다#

REVOKE ALL
ON orders
FROM support_reader;

개념적인 목표는 다음입니다.

support_order
→ 조회 가능

orders
→ 직접 조회 불가

입니다.


28장. 실제 계정으로 허용과 거부를 모두 시험하자#

support_reader 권한을 가진 연결에서:

SELECT
    order_id,
    status
FROM support_order
LIMIT 1;

은 성공해야 합니다.

반면:

SELECT *
FROM orders
LIMIT 1;

은 거부되어야 합니다.

보안 정책은 문서가 아니라 실제 성공·실패 결과로 검증해야 합니다.


29장. 역할 상속 때문에 예상보다 많은 권한을 가질 수 있다#

support_reader에는 원본 권한을 주지 않았습니다.

하지만 사용자가 다른 역할도 가지고 있다고 하겠습니다.

support_reader

+

legacy_all_reader

두 번째 역할이:

orders SELECT

권한을 갖고 있다면 사용자는 원본을 읽을 수 있습니다.

따라서 특정 역할 정의만 보지 말고 실제 로그인 계정의 최종 권한을 확인해야 합니다.


30장. PUBLIC 권한도 확인해야 한다#

특정 사용자에게 권한을 주지 않았더라도:

PUBLIC

에 권한이 남아 있을 수 있습니다.

보안 설계에서는 다음을 함께 봐야 합니다.

직접 권한

역할 상속 권한

PUBLIC 권한

소유자 권한

함수·뷰를 통한 간접 접근

31장. 최소 권한은 새 테이블이 생길 때도 유지되어야 한다#

현재 테이블은 잘 제한했습니다.

그런데 다음 배포에서:

customer_secret

라는 새 테이블이 만들어졌습니다.

기본 권한 설정 때문에 보고 계정이 자동으로 접근할 수 있다면 기존 보안 정책이 깨집니다.

따라서 미래 객체에 적용되는 기본 권한 정책도 점검해야 합니다.


32장. 뷰 하나를 만들었다고 행 단위 권한이 해결되는 것은 아니다#

고객센터 뷰:

SELECT
    order_id,
    customer_id,
    status
FROM orders;

는 민감 열을 제거했습니다.

하지만 고객센터 A가 담당하지 않는 고객의 주문까지 모두 볼 수 있다면 행 범위는 여전히 넓습니다.

열 제한
≠
행 제한

입니다.


33장. 행 수준 정책이 필요한 경우#

예를 들어 상담 직원은 자신에게 배정된 고객만 조회해야 한다고 하겠습니다.

그렇다면:

담당자 A
→ 고객 1, 2, 3

담당자 B
→ 고객 4, 5, 6

처럼 행 단위 접근 정책이 필요합니다.

이를 애플리케이션에서 처리할 수도 있고 DBMS의 행 수준 보안 기능을 검토할 수도 있습니다.


34장. 행 수준 보안도 우회 경로를 시험해야 한다#

정책을 만들었다고 끝나지 않습니다.

실제로:

담당자 A
→ 고객 1 주문
→ 성공

해야 하고:

담당자 A
→ 고객 5 주문
→ 거부

되어야 합니다.

또 다른 뷰나 함수, 관리자 계정, 배치 경로를 통해 우회할 수 없는지도 점검합니다.


35장. 집계 데이터도 개인정보를 드러낼 수 있다#

재무 계정에는 개인 주문을 보여주지 않고 부서별 매출만 보여준다고 하겠습니다.

영업1팀
매출 50만원

문제는 해당 부서에 고객이 한 명밖에 없는 경우입니다.

집계값만으로 그 사람의 거래 금액을 추정할 수 있을 수 있습니다.

따라서:

집계 데이터
=
항상 비식별

이라고 생각해서는 안 됩니다.


36장. 민감 열 마스킹은 노출을 줄이는 방법이다#

고객센터 화면에는 전화번호 전체가 필요하지 않다고 하겠습니다.

원본:

010-1234-5678

화면:

010-****-5678

처럼 표시할 수 있습니다.

하지만 이것이 데이터베이스에 원본이 없는 것을 의미하지는 않습니다.


37장. 화면 마스킹과 저장 데이터 보호는 다르다#

웹 화면에는:

010-****-5678

만 보여도 백엔드가 다음 값을 읽고 있을 수 있습니다.

010-1234-5678

그리고 로그에 원본 전화번호를 남길 수도 있습니다.

따라서 마스킹은:

화면 노출 제한

이지 자동으로:

저장 암호화

권한 제한

로그 보호

까지 해결하는 것은 아닙니다.


38장. 테스트 환경에는 운영 개인정보를 그대로 복사하지 않는 편이 좋다#

개발자가 운영 DB를 그대로 복제해 테스트 서버를 만들었다고 하겠습니다.

테스트 환경은 운영보다 접근 통제가 약한 경우가 많습니다.

그러면:

전화번호

주소

결제 정보

고객명

이 불필요하게 노출될 수 있습니다.

가능하면 업무 목적에 맞는 변환본이나 합성 데이터를 사용하는 것이 좋습니다.


39장. 정적 변환에서는 관계를 유지해야 한다#

운영 고객:

customer_id = 7

을 테스트에서:

customer_id = 9007

로 바꿨다고 하겠습니다.

주문 테이블에서도:

customer_id = 7

을 모두:

9007

로 같은 방식으로 변환해야 합니다.

그렇지 않으면 참조 관계가 끊깁니다.


40장. 개인정보를 가린다고 테스트 특성까지 없애면 안 된다#

실제 운영 데이터에는:

동명이인

전화번호 누락

매우 긴 이름

같은 주소를 사용하는 가족

중복 계정

같은 사례가 있습니다.

모든 고객을:

고객1
고객2
고객3

처럼 단순화하면 이런 경계 사례를 테스트할 수 없습니다.

개인정보는 제거하되 데이터의 구조적 특성은 유지하는 것이 중요합니다.


41장. 전송 중 암호화는 네트워크 경로를 보호한다#

애플리케이션 서버와 DB 서버가 통신합니다.

flowchart LR
    A["Application"] -->|"TLS"| D["Database"]

TLS를 사용하면 네트워크 구간의 도청과 변조 위험을 줄일 수 있습니다.

하지만 단순히 암호화 연결 옵션을 켰다는 것만으로 충분하지 않을 수 있습니다.


42장. 서버 인증서 검증이 중요하다#

클라이언트가 TLS를 사용하지만 아무 서버 인증서나 받아들인다고 하겠습니다.

공격자가 중간에서 다른 서버를 제시할 수 있는 위험이 남습니다.

따라서 실제 운영에서는:

암호화 연결

+

서버 인증서 검증

이 함께 필요합니다.


43장. 전송 암호화는 DB 계정 권한을 제한하지 않는다#

TLS 연결을 사용하고 있습니다.

하지만 애플리케이션 계정이:

SELECT *
FROM customer;

를 자유롭게 실행할 수 있다고 하겠습니다.

TLS는 네트워크에서 데이터를 보호했지만 애플리케이션 자체의 과도한 접근은 막지 못합니다.

TLS
≠
최소 권한

입니다.


44장. 저장 암호화는 저장 장치 유출 경로를 보호한다#

데이터베이스 파일이 저장된 디스크가 탈취됐다고 하겠습니다.

저장 계층 암호화가 적용되어 있다면 원시 저장 장치를 직접 읽는 위험을 줄일 수 있습니다.

하지만 정상적으로 DB에 로그인한 사용자는 권한에 따라 복호화된 데이터를 볼 수 있을 수 있습니다.


45장. 저장 암호화와 컬럼 암호화도 역할이 다르다#

저장 장치 또는 데이터파일 암호화#

보호 대상:

디스크

볼륨

데이터파일

컬럼 또는 애플리케이션 수준 암호화#

보호 대상:

특정 민감 값

예:

주민번호

계좌번호

특정 인증정보

어떤 공격 시나리오를 막으려는지 먼저 정해야 합니다.


46장. 백업 암호화는 별도로 확인해야 한다#

운영 DB는 암호화되어 있습니다.

하지만 백업 파일이:

plain backup

형태로 외부 저장소에 올라간다면 데이터가 유출될 수 있습니다.

따라서 보안 경계를 다음처럼 분리해서 확인해야 합니다.

운영 저장소

백업 저장소

스냅샷

복제본

개발용 사본

47장. 키를 데이터와 같은 위치에 같은 권한으로 두면 보호가 약해진다#

암호화된 백업과 암호화 키가 같은 저장소에 있다고 하겠습니다.

그리고 같은 계정이 둘 다 읽을 수 있습니다.

공격자가 그 계정을 탈취하면:

백업

+

복호화 키

를 함께 얻을 수 있습니다.

따라서 키 관리 경계를 별도로 설계하는 것이 중요합니다.


48장. 암호화 키 수명도 관리해야 한다#

키에는 다음 운영이 필요할 수 있습니다.

생성

배포

사용

회전

폐기

백업

재해 복구

키를 잃어버리면 암호화된 데이터를 복구하지 못할 수 있습니다.

반대로 폐기한 줄 알았던 키가 오래된 서버에 남아 있으면 보안 위험이 남습니다.


49장. 키 회전은 단순히 새 키를 만드는 일만은 아니다#

기존 데이터가 Key A로 암호화되어 있습니다.

새 정책에 따라 Key B로 바꾸기로 했습니다.

질문이 생깁니다.

기존 데이터도 다시 암호화할 것인가?

기존 백업은 어떤 키로 복구하는가?

어느 시점부터 새 키가 사용되는가?

두 키를 얼마나 오래 함께 보관하는가?

키 회전도 데이터 변경 절차입니다.


50장. 암호화된 컬럼의 검색 가능성은 방식에 따라 달라진다#

다음 설명은 지나치게 단순합니다.

암호화하면 검색과 인덱스가 불가능하다.

실제로는 암호화 방식과 요구하는 검색에 따라 다릅니다.

예를 들어 어떤 방식은:

동등 비교

를 가능하게 설계할 수 있습니다.

하지만 같은 평문이 같은 암호문으로 표현되는 구조는 값의 반복 패턴 같은 정보를 노출할 수 있습니다.

즉:

검색 가능성
↔
정보 노출

사이의 교환관계를 이해해야 합니다.


51장. PostgreSQL에서 암호화라고 할 때 계층을 명확히 해야 한다#

다음 표현은 모호합니다.

PostgreSQL 데이터 암호화 적용

무엇을 뜻하는지 명시해야 합니다.

TLS

파일 시스템 암호화

클라우드 볼륨 암호화

백업 암호화

애플리케이션 필드 암호화

확장 기능

각각 보호 경계가 다릅니다.


52장. 로그에도 민감정보가 남을 수 있다#

SQL 로그에 다음과 같이 기록됐다고 하겠습니다.

UPDATE customer
SET phone = '010-1234-5678'
WHERE customer_id = 7;

DB 저장 영역은 잘 보호했지만 로그 파일에 전화번호가 평문으로 남았습니다.

애플리케이션 로그에서도 같은 문제가 생길 수 있습니다.


53장. 쿼리 파라미터를 무조건 로그에 남기면 안 된다#

장애 분석을 위해 입력값을 모두 기록하고 싶을 수 있습니다.

하지만 다음 값이 포함될 수 있습니다.

전화번호

주민번호

인증 토큰

결제 식별자

로그 정책에서도:

무엇을 남길 것인가?

얼마나 오래 보존할 것인가?

누가 볼 수 있는가?

를 정해야 합니다.


54장. 보안 감사 로그와 개인정보 노출은 균형이 필요하다#

감사 로그에는:

누가

언제

어떤 데이터에

어떤 동작을 했는가

가 필요합니다.

하지만 데이터 본문 전체를 그대로 기록할 필요는 없을 수 있습니다.

예:

user=report_user
action=SELECT
object=analytics.daily_sales
result=SUCCESS

처럼 접근 사실 중심으로 기록할 수 있습니다.


55장. 서비스 계정과 관리자 계정을 분리하자#

웹 애플리케이션이 다음 권한까지 가지고 있다고 하겠습니다.

CREATE TABLE

DROP TABLE

ALTER TABLE

GRANT

일반적인 주문 조회 서비스에는 과도한 권한일 가능성이 큽니다.

서비스 계정은:

필요한 SELECT

필요한 INSERT

필요한 UPDATE

로 제한하고 구조 변경은 별도 배포 계정으로 분리할 수 있습니다.


56장. 애플리케이션 침해 시 최소 권한이 피해 범위를 줄인다#

웹 서버가 공격받았다고 하겠습니다.

서비스 계정 권한이:

orders SELECT
orders INSERT
orders 일부 UPDATE

만 있다면 공격자가 DB 연결 정보를 얻더라도 가능한 작업이 제한됩니다.

반면 관리자 권한을 사용하고 있었다면:

전체 테이블 읽기

삭제

권한 변경

스키마 변경

등 더 큰 피해가 가능할 수 있습니다.

최소 권한은 침해 자체를 막는 유일한 방어선은 아니지만 피해 범위를 줄이는 경계입니다.


57장. 비밀정보를 소스코드에 직접 넣지 말자#

다음과 같은 코드가 있다고 하겠습니다.

DB_PASSWORD = "..."

소스 저장소에 비밀번호가 그대로 들어가면:

코드 공유

백업

CI 로그

스크린샷

등을 통해 유출될 수 있습니다.

비밀정보는 전용 비밀 관리 수단이나 적절한 환경 설정으로 분리하는 것이 좋습니다.


58장. 비밀번호를 Git에서 지웠다고 이미 유출된 비밀이 다시 안전해지는 것은 아니다#

한 번 저장소에 커밋된 비밀번호를 현재 파일에서 삭제했습니다.

그러나 Git 이력이나 외부 복제본에 남아 있을 수 있습니다.

따라서 실제 비밀이 노출됐다고 판단되면:

기존 비밀 폐기

새 비밀 발급

사용처 교체

이력 정리 검토

가 필요합니다.


59장. 정적 비밀번호보다 짧은 수명의 자격 증명을 사용할 수 있는 환경도 있다#

클라우드나 조직의 인증 환경에 따라:

짧은 수명 토큰

IAM 기반 인증

동적 DB 자격 증명

같은 방식을 사용할 수 있습니다.

중요한 것은 단순히 비밀번호를 복잡하게 만드는 것보다:

누가 발급하는가?

얼마나 오래 유효한가?

어디에 저장되는가?

폐기가 가능한가?

를 관리하는 것입니다.


60장. 보안 테스트는 공격 문자열만 보내는 것이 아니다#

주문 조회 API의 테스트 목록을 생각해 보겠습니다.

입력 테스트#

정상 주문ID

따옴표 포함 문자열

허용 길이 초과

허용되지 않은 정렬 필드

접근 테스트#

내 주문

다른 고객 주문

관리자 전용 주문

DB 권한 테스트#

허용된 뷰 조회

원본 테이블 조회

쓰기 시도

DDL 시도

보안은 여러 실패 경로를 함께 시험해야 합니다.


61장. 허용 테스트보다 거부 테스트가 더 중요할 때가 있다#

정상 기능 테스트에서는:

report_user
→ daily_sales 조회
→ 성공

을 확인합니다.

하지만 이것만으로는 권한 범위를 알 수 없습니다.

반드시:

report_user
→ customer 원본 조회
→ 거부

report_user
→ DROP TABLE
→ 거부

같은 테스트가 필요합니다.


62장. 역할 설계를 표로 만들면 검증하기 쉽다#

역할 daily_sales orders customer DDL
report_reader 조회 거부 거부 거부
support_reader 거부 제한 뷰 거부 거부
order_service 필요 범위 필요 범위 최소 범위 거부
schema_admin 필요 필요 필요 허용

이 표를 자동화된 권한 테스트로 연결할 수 있습니다.


63장. 보안 정책은 변경될 때마다 다시 시험해야 한다#

처음에는 주문 조회만 있었습니다.

이후 기능이 추가됩니다.

CSV 다운로드

고급 검색

정렬

필터

관리자 조회

엑셀 내보내기

새 기능 하나가 새로운 데이터 노출 경로가 될 수 있습니다.

따라서 기존 보안 검사를 배포 후에도 반복해야 합니다.


64장. CSV 다운로드가 새로운 권한 경계가 될 수 있다#

웹 화면에서는 한 페이지에 20건만 보여줍니다.

하지만 다운로드 기능은:

전체 50만 건

을 내보낼 수 있다고 하겠습니다.

같은 SELECT 권한이라도 대량 추출 위험이 크게 달라집니다.

보안에서는:

데이터 접근 가능 여부

뿐 아니라:

한 번에 얼마나 많이 추출할 수 있는가?

도 고려할 수 있습니다.


65장. 관리자 화면도 최소 권한이 필요하다#

관리자는 모든 것을 볼 수 있다고 단순하게 설계하기 쉽습니다.

하지만 운영 관리자도 역할을 나눌 수 있습니다.

고객 지원 관리자

결제 운영자

DB 관리자

보안 감사자

각자의 업무 범위가 다릅니다.

admin 하나에 모든 권한을 집중하는 구조는 감사와 책임 분리를 어렵게 만들 수 있습니다.


66장. 백업 복구 테스트에서도 권한을 확인해야 한다#

백업 복구 환경에서 실수로:

모든 개발자 접근 가능

상태가 됐다고 하겠습니다.

운영 환경은 안전했지만 복구 테스트 서버에서 개인정보가 노출될 수 있습니다.

따라서 복구 절차에서도:

복원 데이터 암호화

접근 권한

테스트 종료 후 폐기

로그 관리

를 포함해야 합니다.


67장. 마스킹된 데이터도 재식별 가능성을 봐야 한다#

이름을:

김**

전화번호를:

010-****-5678

로 표시했습니다.

하지만 지역, 생년월일, 희귀 직업, 주문 패턴이 함께 있으면 특정 개인을 추정할 수 있을 수도 있습니다.

따라서 단순히 몇 글자를 가렸다고 항상 안전한 비식별 데이터가 되는 것은 아닙니다.


68장. 보안 경계를 한 그림으로 정리하면#

flowchart TD
    U["사용자 입력"] --> V["형식·허용값 검증"]
    V --> Q["매개변수 질의·허용 목록"]
    Q --> A["인증·업무 권한 확인"]
    A --> R["최소 권한 DB 역할"]
    R --> D["행·열 제한"]
    D --> T["TLS 전송"]
    T --> S["저장·백업 보호"]
    S --> L["로그·감사·키 관리"]

어느 한 단계만으로 전체 보안을 설명할 수 없습니다.


69장. SQL 인젝션 대응 체크리스트#

  1. SQL 값 입력에 매개변수 바인딩을 사용하는가?
  2. 문자열 연결로 SQL을 만드는 곳이 남아 있지 않은가?
  3. 저장 프로시저 내부의 동적 SQL도 확인했는가?
  4. 정렬 열·테이블명 같은 구조 입력은 허용 목록인가?
  5. 입력 길이와 형식을 검증하는가?
  6. 오류 메시지가 내부 SQL 구조를 과도하게 노출하지 않는가?
  7. DB 계정 권한이 공격 성공 시 피해 범위를 제한하는가?
  8. 동적 검색 기능 추가 시 다시 점검하는가?

70장. 최소 권한 체크리스트#

  1. 서비스 계정에 DDL 권한이 필요한가?
  2. 보고 계정이 원본 고객 테이블을 읽을 수 있는가?
  3. 직접 권한 외에 역할 상속을 확인했는가?
  4. PUBLIC 권한이 남아 있는가?
  5. 새 객체에 자동으로 과도한 권한이 생기지 않는가?
  6. 뷰에서 불필요한 민감 열을 제거했는가?
  7. 행 수준 범위도 제한되는가?
  8. 관리자 계정을 업무별로 분리할 수 있는가?
  9. 허용·거부 테스트를 실제 계정으로 수행했는가?
  10. 퇴직자나 역할 변경 사용자의 권한이 회수되는가?

71장. 암호화 체크리스트#

전송#

TLS를 사용하는가?

서버 인증서를 검증하는가?

저장#

데이터파일과 디스크를 어떻게 보호하는가?

백업#

백업도 별도로 보호되는가?

필드#

특정 민감값에 추가 암호화가 필요한가?

키#

키와 데이터가 분리되어 있는가?

회전과 복구가 가능한가?

72장. 로그·테스트 데이터 체크리스트#

로그에 개인정보가 남는가?

SQL 파라미터를 그대로 기록하는가?

운영 DB를 테스트로 그대로 복제하는가?

마스킹 후에도 재식별 가능성이 있는가?

테스트 데이터의 참조 관계가 유지되는가?

테스트 종료 후 데이터가 폐기되는가?

운영 DB만 보호하고 주변 복사본을 방치하면 전체 보안 수준은 올라가지 않습니다.


73장. 보안 사고를 한 계층에서만 해석하면 원인을 놓친다#

예를 들어 다른 고객의 주문이 노출됐습니다.

가능한 원인은 여러 가지입니다.

SQL 인젝션

객체 수준 권한 과다

행 수준 권한 오류

API 소유권 검사 누락

잘못된 캐시 키

보고용 뷰 설계 오류

따라서:

SQL 인젝션이 없으니 DB 보안은 안전하다.

라고 결론 내리면 안 됩니다.


74장. 반대로 인젝션 취약점이 있어도 최소 권한은 피해를 줄일 수 있다#

애플리케이션에 SQL 인젝션 취약점이 존재했다고 가정하겠습니다.

서비스 계정이:

orders 일부 조회

만 가능하다면 공격 범위가 제한될 수 있습니다.

반면 서비스 계정이 관리자라면 피해가 훨씬 커집니다.

즉:

입력 방어

+

최소 권한

은 서로 대체 관계가 아니라 방어 계층입니다.


75장. 암호화 역시 다른 보안 통제를 대체하지 않는다#

저장 장치가 완벽하게 암호화되어 있습니다.

하지만 도난당한 애플리케이션 계정으로 정상 로그인하면 DBMS가 데이터를 복호화해 제공할 수 있습니다.

반대로 계정 권한이 잘 설계되어 있어도 암호화되지 않은 백업 파일이 유출될 수 있습니다.

따라서:

접근 통제

암호화

입력 보안

감사

백업 보호

를 분리해서 생각해야 합니다.


76장. 보안 설계를 실제 실패 조건으로 문서화하자#

다음과 같이 적으면 테스트하기 쉽습니다.

report_user는
analytics.daily_sales만 SELECT 가능해야 한다.

support_reader는
support_order 뷰는 읽을 수 있어야 한다.

support_reader의 orders 직접 조회는 실패해야 한다.

고객 7은
고객 8의 주문을 읽을 수 없어야 한다.

order_service는
DROP TABLE을 실행할 수 없어야 한다.

이 문장은 정책이면서 테스트 명세가 됩니다.


77장. 보안 정책을 자동 테스트와 연결하면 변경에 강해진다#

새 배포로 권한이 바뀌었다고 하겠습니다.

수동 검토만 하면 놓칠 수 있습니다.

배포 테스트에서:

허용 SQL
→ 반드시 성공

금지 SQL
→ 반드시 실패

를 확인하면 권한 회귀를 조기에 발견하기 쉽습니다.


78장. 핵심 정리#

데이터베이스 보안은 SQL 인젝션 하나를 막는 작업으로 끝나지 않습니다.

첫 번째 경계는 사용자 입력과 SQL 구조를 분리하는 것입니다.

검색값
→ 매개변수 바인딩

정렬 열
→ 허용 목록

테이블 선택
→ 서버 내부 매핑

처럼 입력 유형에 따라 방식을 구분해야 합니다.

특히 매개변수 바인딩은 값 전달에는 적합하지만:

열 이름

테이블 이름

정렬 방향

같은 SQL 구조 요소를 자유롭게 안전화해 주는 만능 기능은 아닙니다.

두 번째 경계는 접근 권한입니다.

SQL이 안전하게 실행되더라도:

고객 7
→ 고객 8의 주문 조회

가 가능하다면 보안은 실패한 것입니다.

따라서:

인증된 사용자

데이터 소유자

역할

행 범위

열 범위

를 함께 확인해야 합니다.

세 번째는 최소 권한입니다.

report_reader
→ 승인된 집계만

support_reader
→ 제한된 주문 뷰만

order_service
→ 주문 처리에 필요한 동작만

schema_admin
→ 구조 변경

처럼 계정 역할을 분리하면 한 계정이 침해되더라도 피해 범위를 줄일 수 있습니다.

네 번째는 암호화 경계입니다.

TLS
→ 전송 경로

저장 암호화
→ 디스크·파일

백업 암호화
→ 복사본

필드 암호화
→ 특정 민감값

은 서로 다른 유출 경로를 보호합니다.

하나를 적용했다고 나머지가 필요 없어지는 것은 아닙니다.

다섯 번째는 주변 데이터입니다.

로그

백업

테스트 DB

CSV 다운로드

복제본

에도 민감정보가 남을 수 있습니다.

운영 데이터베이스만 강하게 보호하고 테스트 사본이나 로그를 방치하면 전체 보안 수준은 낮아집니다.

마지막으로 가장 중요한 원칙은 이것입니다.

보안 정책은 “무엇이 가능해야 하는가”뿐 아니라 “무엇이 반드시 실패해야 하는가”까지 정의해야 한다.

예를 들어:

report_user
→ daily_sales 조회 성공

report_user
→ customer 원본 조회 실패

고객 7
→ 자신의 주문 조회 성공

고객 7
→ 고객 8 주문 조회 실패

order_service
→ 필요한 UPDATE 성공

order_service
→ DROP TABLE 실패

처럼 허용과 거부를 함께 시험해야 합니다.

좋은 데이터베이스 보안 설계는 공격 문자열을 많이 알고 있는 시스템이 아닙니다.

입력이 SQL 구조가 되지 못하게 하고, 계정이 필요한 데이터만 읽게 하며, 민감 데이터가 이동하고 저장되고 복제되는 모든 경계에서 노출 범위를 통제하는 시스템입니다.

그리고 새 기능이 추가될 때마다 다시 물어야 합니다.

이 기능으로 사용자가 새 SQL 구조를 선택할 수 있게 됐는가?

이 기능으로 이전보다 더 많은 행이나 열을 볼 수 있게 됐는가?

이 기능으로 새로운 복사본이나 로그가 생겼는가?

이 세 질문을 반복해서 확인하는 것이 실제 운영에서 데이터베이스 보안을 유지하는 가장 중요한 습관입니다.

이 페이지의 목차