데이터베이스는 데이터를 저장하고 조회하는 시스템이다. 오랫동안 그 표준은 관계형(SQL) DB였고, NoSQL은 그 대안으로 나온 부류다. 이 글은 NoSQL이 뭐고, SQL과 무엇이 다르고, 그래서 보안상 왜 다른 취약점이 생기는지를 배경지식 없이도 따라오게 순서대로 잇는다.

기준점: 관계형(SQL)

비교 대상을 먼저 세우자. 관계형 DB(MySQL, PostgreSQL 등)는 데이터를 표(테이블)로 저장한다. 행과 열이 있고, 어떤 열이 어떤 타입인지를 미리 정한 설계도(스키마)가 고정돼 있다. 조회는 SQL이라는 문자열 언어로 한다.

1
SELECT * FROM users WHERE id = 'admin'

구조가 엄격한 대신 데이터 간 관계와 정합성을 관리하는 데 강하다. 그리고 취약점(SQL Injection)도 이 성질에서 나온다 — 쿼리가 문자열이라, 그 문자열 조립에 문법을 끼워넣는 공격이다.

NoSQL은 왜 나왔나

웹 서비스가 커지면서 두 가지가 필요해졌다. 하나는 스키마를 미리 못 정하거나 자주 바뀌는 데이터를 유연하게 담는 것, 다른 하나는 서버 대수를 옆으로 늘려 부하를 나누는 수평 확장이다. 관계형의 엄격한 표 구조가 이 둘에 걸림돌이 됐고, 그래서 “Not only SQL”, 즉 스키마가 유연하고 확장이 쉬운 DB들이 등장했다. 그게 NoSQL이다.

핵심 성질은 두 가지다. 스키마가 없어서 문서마다 필드가 달라도 되고, 데이터를 표가 아니라 대개 JSON 같은 문서 형태로 저장한다.

종류

  • 문서형(Document): JSON 문서 단위로 저장. MongoDB, CouchDB. 가장 흔하다.
  • 키-값형(Key-Value): 단순한 key → value 저장. Redis.
  • 컬럼형(Wide-column): Cassandra.
  • 그래프형(Graph): 데이터보다 관계 자체를 저장. Neo4j.

결정적 차이: 쿼리를 문자열이 아니라 구조(객체)

SQL은 쿼리가 문자열 한 줄이었다. 반면 문서형 NoSQL은 조회 조건을 대개 구조화된 객체로 준다. 예를 들어 MongoDB에서 “id가 admin인 사용자"는 이렇게 쓴다.

1
db.users.find({ id: "admin" })

문자열이 아니라 객체 { id: "admin" }이다. 그리고 이 객체 안에는 값뿐 아니라 연산자도 들어갈 수 있다.

1
db.users.find({ id: { $ne: null } })   // id가 null이 아닌 문서 전부

$ne(같지 않음), $gt(큼), $regex(정규식) 같은 연산자가 쿼리의 데이터 안에 섞여 동작한다. 바로 이 성질이 뒤에 나올 취약점의 씨앗이다.

취약점도 다르다: NoSQL Injection

SQL Injection이 문자열 쿼리에 문법을 주입하는 것이라면, NoSQL Injection은 입력의 타입과 구조를 조작하는 것이다. 뿌리는 하나다. 웹에서 입력을 JSON으로 받으면, 원래 문자열이어야 할 값이 객체·배열·null이 될 수 있다는 점.

폼(urlencoded)으로 보내면 값이 전부 문자열이지만, 요청을 Content-Type: application/json으로 보내면 값의 타입을 마음대로 정할 수 있다. 그래서 모든 NoSQL 문제의 첫수는 “바디를 JSON으로 바꿔 타입의 자유를 확보하는 것"이다. 이걸 이용한 대표 공격 둘:

  • 연산자 주입: 로그인에서 비밀번호 자리에 문자열 대신 { "$ne": null }을 넣으면, 쿼리가 “비번이 null이 아닌 사용자"로 바뀌어 인증이 통째로 뚫린다. { "$regex": "^a" }처럼 넣어 값을 한 글자씩 추출하기도 한다.
  • 타입 혼동: 앱이 문자열을 가정하고 짠 로직에 배열이나 객체, null을 넣어 흐름 자체를 깨뜨린다.

반드시 구분할 것: 비교가 DB 안이냐, 앱 코드냐

NoSQL Injection에서 제일 많이 헷갈리는 지점이다. $ne 같은 연산자 주입이 통하려면, 그 비교가 DB 쿼리 안에서 일어나야 한다(db.find({ pw: 입력 })). 만약 코드가 문서를 먼저 꺼낸 뒤 앱 코드에서 비교한다면(예: JS if (doc.pw === 입력)), 연산자는 DB에 닿지도 못해서 안 먹힌다. 이때는 연산자 대신 언어 자체의 허점을 노려야 한다. 백엔드가 Node라면 JS의 ===, undefined, 타입 저글링 같은 것들이다.

정리하면 NoSQL 문제의 사고 순서는 이렇다. 먼저 입력을 JSON으로 바꿔 타입의 자유를 확보한다. 그다음 비교가 DB에서 되는지 앱 코드에서 되는지 본다. DB에서 되면 연산자 주입, 앱 코드에서 되면 타입·언어 허점을 노린다.