ALTER TABLE을 실행했을 뿐인데 시스템이 갑자기 승인 요청서를 쓰라고 했다.

조금 이상하다.

ALTER TABLE은 테이블 구조를 바꾸는 DDL이다. 반면 승인 정책은 행을 추가하고 고치고 지우는 DML을 통제하려고 만든 것이었다.

정책이 지키려던 것
INSERT / UPDATE / DELETE

정책이 갑자기 붙잡은 것
ALTER TABLE

원인은 비교적 빨리 보였다. SQL 분석기가 똑똑해지면서 예전에는 보지 못하던 DDL 대상 테이블까지 정확히 찾아내기 시작했다. 그런데 뒤쪽의 정책 엔진은 새 분석 결과를 구분하지 못했다.

“테이블이 잡혔네? 승인 대상이군.”

여기까지라면 수정도 간단해 보인다.

if (statementType is not Insert and not Update and not Delete)
{
    return;
}

DDL만 승인 정책에서 빼면 끝이다.

그런데 이 코드를 넣기 직전, 문 옆에서 조용히 손을 들고 있는 SQL이 하나 보였다.

REPLACE INTO users (id, name)
VALUES (1, 'Mina');

REPLACE였다.

ALTER를 풀어주려고 만든 작은 allowlist가, 이번에는 실제 데이터를 바꾸는 REPLACE를 승인 없이 통과시킬 수 있었다.

오탐 하나를 고치려다가 미탐 하나를 만드는 순간이다.

DDL과 DML, 30초만에 이해하기

먼저 용어부터 간단히 정리하자.

DDL은 데이터가 들어갈 그릇의 구조를 바꾼다.

ALTER TABLE users ADD COLUMN nickname VARCHAR(50);
DROP TABLE old_users;
TRUNCATE TABLE temp_logs;

DML은 그릇 안의 데이터를 읽거나 바꾼다.

SELECT * FROM users;
INSERT INTO users ...;
UPDATE users SET ...;
DELETE FROM users ...;

접근통제와 승인 시스템은 이 차이를 매우 중요하게 본다.

테이블에 열을 추가할 수 있는 권한과 고객 데이터를 수정할 수 있는 권한은 같은 권한이 아니다. 감사 로그에서도 “구조 변경”과 “데이터 변경”은 다른 사건이다.

문제는 SQL 세계가 교과서의 네 칸짜리 표보다 훨씬 넓다는 데 있다.

INSERT
UPDATE
DELETE
REPLACE
UPSERT
MERGE INTO
WITH ... UPDATE
prepared statement
여러 SQL이 섞인 batch

이름은 달라도 실제 데이터를 바꾸는 문장이 여럿 있다.

그래서 보안 정책에서 “DML은 세 종류”라고 단정하는 순간, 틈이 생길 수 있다.

SQL 분석기가 똑똑해졌는데 왜 장애가 났을까

처음에는 SQL 분석기가 DDL을 잘 못 알아봤다고 생각하기 쉽다.

하지만 실제로는 반대였다.

분석기는 이전보다 일을 더 잘했다.

ALTER TABLE accounts ADD COLUMN memo TEXT
                  │
                  └─ 대상 테이블: accounts

이 정보는 유용하다.

  • 사용자가 해당 테이블의 구조를 변경할 권한이 있는지 검사할 수 있다.
  • 감사 로그에 어떤 객체가 바뀌었는지 남길 수 있다.
  • 위험한 DDL을 별도 정책으로 차단할 수 있다.

그러므로 “DDL 분석 때문에 문제가 생겼다”며 분석 기능을 되돌리는 것은 좋은 해결이 아니다.

문제는 그 결과를 소비하는 정책 엔진에 있었다.

SQL 분석기
  -> 대상 테이블을 찾음

권한 정책
  -> DDL 권한 검사에 사용

감사 로그
  -> 변경 대상 기록에 사용

DML 승인 정책
  -> SQL 종류를 구분하지 않고 함께 사용  ← 문제

같은 분석 결과라도 소비자마다 필요한 범위가 다르다.

택배 기사가 주소를 정확히 읽는 것은 좋은 일이다. 그렇다고 그 주소로 오는 모든 사람에게 냉장식품 창고 출입증을 발급하면 안 된다.

분석 정확도와 정책 적용 범위는 별개의 계약이다.

가장 쉬운 패치가 가장 위험해지는 순간

DDL을 제외하는 가장 단순한 코드는 보통 이런 모양이다.

var approvalTypes = new[]
{
    SqlStatementType.Insert,
    SqlStatementType.Update,
    SqlStatementType.Delete
};

if (!command.StatementTypes.Any(approvalTypes.Contains))
{
    return null;
}

겉으로는 훌륭하다.

  • ALTER는 빠진다.
  • DROP도 빠진다.
  • RENAME도 빠진다.
  • 기존 INSERT, UPDATE, DELETE는 계속 승인받는다.

하지만 질문을 한 번 바꿔야 한다.

승인받아야 하는 SQL 세 개가 무엇인가?

이 질문보다 더 안전한 질문은 이것이다.

데이터를 변경할 수 있는 모든 실행 경로가 무엇인가?

그 순간 숨어 있던 문장들이 나타난다.

REPLACE
UPSERT
MERGE INTO

DBMS마다 지원 범위와 의미는 다르다. 그래도 정책을 바꿀 때 이들을 확인하지 않는 것은 위험하다.

REPLACE는 INSERT의 사촌이 아니다

MySQL의 REPLACE를 이름만 보면 “있으면 바꾸고, 없으면 넣는 편리한 INSERT”처럼 느껴진다.

하지만 동작은 더 묵직하다.

기존 행이 unique key나 primary key와 충돌하면, 기존 행을 제거한 뒤 새 행을 넣을 수 있다.

REPLACE
  -> 충돌 행 확인
  -> 기존 행 제거 가능
  -> 새 행 삽입

즉 권한 관점에서도 단순 조회가 아니다. 데이터 변경 효과가 분명하다.

그런데 승인 정책의 allowlist가 세 문장만 안다면 어떻게 될까?

INSERT  -> 승인 필요
UPDATE  -> 승인 필요
DELETE  -> 승인 필요
REPLACE -> 목록에 없음 -> 승인 정책 건너뜀

ALTER TABLE 오탐은 사라진다.

대신 REPLACE 미탐이 생긴다.

사용자 경험 관점에서는 버그 하나를 고친 것처럼 보일 수 있다. 보안 관점에서는 승인 경로 하나를 열어버린 셈이다.

이것이 접근통제 코드의 무서운 점이다.

실패가 꼭 에러 화면으로 나타나지 않는다. 너무 조용히 성공하는 것도 실패다.

allowlist가 나쁜 것이 아니다

여기서 오해하면 안 된다.

보안 정책에는 denylist보다 allowlist가 더 안전한 경우가 많다.

denylist
알려진 위험한 것만 막는다
새 SQL이 생기면 통과할 수 있다

allowlist
검증한 것만 정책 대상으로 인정한다
범위를 명확하게 설명할 수 있다

문제는 allowlist 자체가 아니라, 목록을 만드는 기준이 불완전한 것이다.

좋은 allowlist는 이름을 기억해서 만든 목록이 아니다. 제품의 행동 계약에서 나온 목록이다.

예를 들면 이렇게 정의해야 한다.

승인 정책 대상
= 지원 DBMS에서 실제 데이터를 변경하는 statement category

그 다음 각 DBMS에서 이 계약에 해당하는 문장을 매핑한다.

공통 후보
- INSERT
- UPDATE
- DELETE
- REPLACE
- UPSERT
- MERGE INTO

모든 DBMS가 모든 문장을 지원하는 것은 아니다. 중요한 것은 “다 넣자”가 아니라 “왜 넣거나 빼는지 증명하자”다.

공통 서비스 한 곳을 고칠 때 생기는 파장

또 하나의 함정은 코드가 작다고 영향도 작다고 생각하는 것이다.

정책 엔진에서는 한 서비스가 여러 기능에 공유되는 경우가 많다.

공통 Workflow Requirement 서비스
  ├─ 기존 테이블 변경 승인 정책
  └─ 태그 기반 DML 승인 정책

여기에 statement filter를 넣으면 두 기능이 함께 바뀐다.

한쪽만 보고 테스트하면 다른 쪽의 계약을 깨뜨릴 수 있다.

게다가 분석 결과는 실행 차단에만 쓰이지 않을 수 있다.

정책 분석 결과
  ├─ 실행 허용/차단
  ├─ 승인 요청 전환
  ├─ 감사 로그 상태
  └─ 어떤 승인 규칙이 적용됐는지 기록

DDL을 승인 정책에서 제외했더니 실행은 정상화됐지만, 감사 대상 객체까지 사라진다면 그것도 잘못된 패치다.

원하는 결과는 이렇다.

ALTER TABLE
  - DML 승인 요청: 없음
  - DDL 권한 검사: 유지
  - 대상 테이블 감사 기록: 유지
  - 잘못된 승인 차단 기록: 제거

정책 하나를 끄는 것이 아니라, 소비자별 책임을 분리하는 일이다.

테스트는 SQL 한 줄이 아니라 행렬이어야 한다

이런 패치에 ALTER TABLE 테스트 하나만 추가하면 마음은 편해진다.

하지만 안전해지지는 않는다.

최소한 세 축이 필요하다.

1. 문장 축

SQL 확인할 것
SELECT 조회 목적 정책이 그대로인가
INSERT 승인 요구가 유지되는가
UPDATE 승인 요구가 유지되는가
DELETE 승인 요구가 유지되는가
REPLACE 승인 경로를 우회하지 않는가
UPSERT 지원 DBMS의 기존 계약이 유지되는가
MERGE INTO 지원 DBMS의 기존 계약이 유지되는가
ALTER DML 승인은 없고 DDL 권한은 유지되는가
DROP DML 승인은 없고 DDL 권한은 유지되는가
RENAME 대상 객체 감사가 유지되는가
TRUNCATE 제품이 정한 DDL 계약대로 동작하는가

2. 정책 축

- 기존 테이블 기반 승인 정책
- 태그 기반 승인 정책
- 정책이 없는 테이블
- 여러 테이블이 섞인 SQL
- exact match와 pattern match

3. 실행 경로 축

- 웹 SQL 편집기
- 외부 DB client
- 승인된 요청의 재실행
- prepared statement
- 여러 SQL이 섞인 batch
- 대체 분석 SQL을 사용하는 경로

테스트 수가 갑자기 많아 보인다.

하지만 이것이 바로 “코드 변경량은 작고, 안전한 완성 난이도는 중간”인 이유다.

패치의 난이도는 수정한 줄 수로 결정되지 않는다. 지켜야 할 계약의 수로 결정된다.

batch는 특히 조심해야 한다

다음 두 문장이 한 요청에 들어왔다고 해보자.

UPDATE accounts SET status = 'ACTIVE' WHERE id = 1;
ALTER TABLE accounts ADD COLUMN memo TEXT;

코드가 이렇게 판단한다면 어떨까?

if (command.StatementTypes.Any(IsApprovalTarget))
{
    AnalyzeWholeCommand();
}

UPDATE가 있으므로 전체 command를 승인 분석한다.

그 과정에서 ALTER TABLE의 대상 테이블까지 다시 승인 정책에 섞일 수 있다.

반대로 All을 사용하면 DML과 DDL이 함께 있다는 이유로 UPDATE까지 승인 분석에서 빠질 수 있다.

Any  -> DDL target이 섞일 위험
All  -> DML이 빠질 위험

정답은 보통 boolean 한 글자를 고르는 것이 아니다.

statement별 분석 경계를 유지하는 것이다.

Statement 1: UPDATE
  -> DML 승인 분석

Statement 2: ALTER
  -> DDL 권한 분석

batch를 지원하는 시스템이라면 command 전체의 타입 목록만 보고 조기 반환하는 패치는 반드시 실제 분리 구조를 확인해야 한다.

좋은 패치는 네 개의 질문에 답한다

비슷한 정책 회귀를 만났다면 코드부터 고치기 전에 다음 질문을 던져보자.

1. 분석기가 잘못 본 것인가, 소비자가 잘못 해석한 것인가?

분석 결과가 다른 권한과 감사 기능에는 필요할 수 있다. 생산자를 되돌리기 전에 소비 경계를 확인한다.

2. 정책의 범위는 문장 이름인가, 행동 category인가?

INSERT/UPDATE/DELETE라는 이름 목록보다 “데이터를 변경하는 모든 경로”라는 계약이 먼저다.

3. 같은 서비스를 누가 공유하는가?

공통 서비스 한 줄은 여러 정책 소스와 DBMS를 동시에 바꿀 수 있다.

4. 실패가 차단으로만 나타나는가?

보안 시스템에서는 잘못 막는 false positive와 잘못 통과시키는 false negative가 모두 실패다.

false positive
정상 DDL을 승인 대상으로 오판

false negative
실제 DML을 승인 없이 통과

둘 중 두 번째는 사용자에게 에러를 보여주지 않아서 더 늦게 발견될 수 있다.

작은 수정, 큰 계약

이번 문제의 재미있는 점은 코드만 보면 정말 작다는 것이다.

statement type을 확인하고, 대상이 아니면 분석을 건너뛴다. 몇 줄이면 쓸 수 있다.

하지만 안전한 패치가 되려면 훨씬 많은 것을 알아야 한다.

  • SQL 종류별 실제 데이터 변경 효과
  • DBMS별 문법 차이
  • 공통 정책 서비스의 소비자
  • 권한 검사와 승인 검사의 경계
  • 감사 로그가 분석 결과를 사용하는 방식
  • batch와 prepared statement의 실행 구조

그래서 접근통제 코드에서 이런 문장을 기억해둘 만하다.

작은 패치의 위험은 코드 크기가 아니라, 보이지 않는 계약의 크기에서 나온다.

ALTER TABLE을 풀어주는 데 성공했다고 끝이 아니다.

그 순간 REPLACE가 옆문으로 나가고 있지는 않은지 봐야 한다.

좋은 보안 패치는 정상 작업을 덜 막으면서도, 보호해야 할 데이터 변경과 승인 근거를 놓치지 않는다.

즉 두 가지를 함께 지켜야 한다.

Accountability
누가 어떤 데이터를 왜, 어떤 승인으로 바꿨는가

Protection
승인 범위 밖의 변경은 차단되고 민감한 데이터는 보호되는가

오탐을 줄이는 것과 통제를 유지하는 것.

둘 다 성공해야 비로소 버그가 고쳐진다.