DEV Community

Cover image for API 악성 입력 테스트 방법: 공격자보다 먼저 대비하기
Rihpig
Rihpig

Posted on • Originally published at apidog.com

API 악성 입력 테스트 방법: 공격자보다 먼저 대비하기

핵심 요약: API 입력은 공격 표면이므로 그에 맞춰 테스트해야 합니다. 오버사이즈 필드, 잘못된 유형, 잘못된 형식의 본문, 인젝션 문자열을 보내는 부정 케이스를 작성한 다음, 엔드포인트가 4xx 응답을 하고 절대 5xx를 반환하지 않는지 확인하세요. 스키마 유효성 검사를 additionalProperties: false, 열거형(enums), 길이 제한을 사용한 보안 통제로 만드세요. 모든 변경사항에 대해 CI에서 전체 테스트 스위트를 실행하세요. AI 에이전트는 이를 시급하게 만듭니다. AI 에이전트는 머신 속도로 페이로드를 생성하고 전달하므로, "이 데이터 로드"가 조용히 "이 코드 실행"으로 변하는 일이 이제 대규모로 발생할 수 있습니다.

대부분의 테스트 스위트는 호출자가 정상적인 경우 API가 작동한다는 것만 증명합니다. 유효한 본문을 보내면 200 응답을 받고 어설션은 통과합니다. 하지만 이 결과는 본문이 적대적일 때 어떤 일이 발생하는지 거의 알려주지 않습니다. 신뢰할 수 없는 입력은 엔드포인트가 스스로 생성하지 않은 모든 데이터입니다. 요청 본문, 쿼리 문자열, 헤더, 파일 업로드, 웹훅 페이로드, AI 에이전트가 즉석에서 조립하는 JSON이 여기에 포함됩니다. 이 모든 입력은 결국 누군가가 최악의 형태로 보낼 것이라는 가정으로 처리해야 합니다.

지금 Apidog을 사용해 보세요

2026년 7월, Hugging Face는 도난당한 비밀번호가 아닌 데이터가 진입 벡터였던 보안 사고를 설명했습니다. 해당 침해로부터 얻은 교훈은 별도로 다뤘으며, 이 글은 그 실질적인 후속 가이드입니다.

이 글에서는 공격자가 보낼 만한 입력을 직접 전송하는 테스트를 만들고, 모든 변경사항에 대해 자동으로 실행하는 방법을 다룹니다. 이 범주는 OWASP API 보안 톱 10과 일치합니다. Apidog은 이런 테스트를 위한 계약을 설계하고 실행하는 한 가지 방법이지만, 핵심 원칙은 이미 사용 중인 어떤 프레임워크에도 적용됩니다.

입력은 폼 필드가 아닌 공격 표면입니다

유효성 검사는 종종 사용자 경험을 위한 기능으로 취급됩니다. 빈 이메일을 감지하고 빨간 테두리를 표시하는 식입니다. 하지만 API 관점에서는 충분하지 않습니다.

API가 허용하는 모든 필드는 호출자가 깨뜨릴 수 있는 약속입니다. 깨진 약속 하나하나는 애플리케이션 로직으로 이어지는 경로가 될 수 있습니다.

  • 작은 정수여야 하는 limit999999999가 됩니다.
  • 단순한 파일명이어야 하는 filename../../etc/passwd가 됩니다.
  • 설정 객체여야 하는 config가 명령이나 템플릿 표현식이 됩니다.

보안 테스트는 마지막에 추가하는 별도 작업이 아닙니다. 이미 작성하고 있는 부정 테스트를, 가장 위험한 필드에 적용하는 작업입니다.

각 입력 필드에 다음 질문을 적용하세요.

이 필드에 들어갈 수 있는 최악의 값은 무엇인가?

이 질문을 습관화하면 API 보안 모범 사례의 상당수를 실천하게 됩니다.

"이 데이터 로드"가 "이 코드 실행"으로 변한 방법

Hugging Face 사고는 입력이 왜 엄격하게 다뤄져야 하는지 보여주는 사례입니다. Hugging Face는 악성 데이터셋이 진입 벡터였다고 밝혔습니다. 조작된 데이터셋이 원격 코드 데이터셋 로더를 트리거했고, 데이터셋 구성 내부에는 템플릿 인젝션이 존재했습니다. 자세한 내용은 Hugging Face의 보안 사고 보고서에서 확인할 수 있습니다.

실패 흐름은 다음과 같습니다.

  1. 엔드포인트가 데이터로 설명된 객체를 수락합니다.
  2. 애플리케이션이 해당 데이터를 로드합니다.
  3. 로드 과정에서 공격자가 제어하는 명령을 실행할 수 있는 코드 경로가 활성화됩니다.

즉, "이 데이터 로드"가 "이 코드 실행"으로 바뀐 것입니다.

템플릿 인젝션도 같은 구조입니다. 단순 텍스트여야 하는 구성 값이 평가되면, 텍스트가 실행 가능한 입력으로 변합니다.

핵심은 특정 조직의 드문 실수가 아닙니다. 로더 이름, 형식, 템플릿, 직렬화된 객체, 구성 블롭을 허용하는 엔드포인트는 의도하지 않았더라도 명령 실행 경로를 만들 수 있습니다.

적대적인 구성을 해당 엔드포인트로 보내는 테스트가 없다면, 입력이 비활성 데이터로만 유지된다는 가정을 검증하지 않은 것입니다. 검증되지 않은 가정 자체가 취약점이 됩니다.

보안 통제로서의 스키마 유효성 검사

가장 저렴하게 추가할 수 있는 통제 중 하나는 API 경계에서의 엄격한 스키마 검증입니다.

스키마는 문서화 도구만이 아닙니다. 스키마와 일치하지 않는 요청을 거부하면, 비즈니스 로직이 요청을 처리하기 전에 입력을 차단하는 필터가 됩니다. JSON Schema는 이 필터를 엄격하게 구성할 수 있는 기본 요소를 제공합니다.

다음은 데이터셋 구성에 적용할 수 있는 예시 스키마입니다.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "additionalProperties": false,
  "required": ["loader", "name"],
  "properties": {
    "loader": { "enum": ["csv", "json", "parquet"] },
    "name": {
      "type": "string",
      "maxLength": 128,
      "pattern": "^[\\w .-]+$"
    },
    "rows": {
      "type": "integer",
      "minimum": 0,
      "maximum": 1000000
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

이 스키마는 네 가지 방어 계층을 제공합니다.

통제 차단하는 입력
additionalProperties: false 숨겨진 template, command 같은 예상 외 필드
loader 열거형 pickle:// 같은 허용되지 않은 원격 코드 로더
maxLength 메모리 고갈을 노리는 수 메가바이트 문자열
pattern {{, '; DROP TABLE 같은 형식 밖 문자열

이 규칙들은 공격자를 특별히 식별하지 않습니다. 대신 애플리케이션이 실제로 지원하는 좁은 입력 범위만 허용합니다. 이 제한 자체가 보안 속성입니다.

물론 계약 검증만으로 모든 익스플로잇을 막을 수는 없습니다. 다만 "이 엔드포인트가 무엇을 허용하는지 확인하지 않았다"라는 흔하고 위험한 버그 범주를 효과적으로 줄일 수 있습니다.

부정 테스트: 엔드포인트가 "안 돼"라고 말한다는 것을 증명하기

해피 패스 테스트는 좋은 입력이 좋은 출력을 만드는지 검증합니다. 부정 테스트는 잘못된 입력이 통제된 거부를 만드는지 검증합니다.

거부는 기능입니다.

  • 명확한 오류와 함께 반환되는 400 또는 422는 API가 경계를 지키고 있다는 뜻입니다.
  • 500은 적대적인 입력이 처리 준비가 되지 않은 로직까지 도달했다는 뜻입니다.

각 필드에 대해 다음 실패 클래스를 정의하세요.

  1. 잘못된 유형
  2. 필수 필드 누락
  3. 금지된 필드 존재
  4. 너무 긴 값
  5. 허용 범위를 벗어난 값
  6. 필드 형식에 맞는 인젝션 문자열
  7. 잘못된 콘텐츠 유형 또는 손상된 본문

각 테스트에서는 최소한 다음 두 가지를 확인해야 합니다.

assert response.status_code in (400, 413, 415, 422)
assert response.status_code < 500
Enter fullscreen mode Exit fullscreen mode

가능하다면 부작용이 발생하지 않았다는 것도 검증하세요. 예를 들어 생성 API라면 거부된 요청 이후 리소스가 생성되지 않았는지 확인할 수 있습니다.

assert not dataset_was_created(request_id)
Enter fullscreen mode Exit fullscreen mode

오류 메시지의 정확한 문구보다는 동작을 어설션하세요. 예를 들어 "invalid loader"라는 텍스트 자체를 테스트하면 무해한 문구 변경에도 테스트가 깨집니다. 상태 코드, 부작용 여부, 민감한 출력 노출 여부를 우선 검증하는 편이 더 안정적입니다.

필드별 시작 목록은 API 보안 테스트 체크리스트에서도 확인할 수 있습니다.

전용 테스트를 할 가치가 있는 인젝션 클래스

몇몇 인젝션 계열은 너무 자주 발생하므로 일회성 수동 검사가 아닌 고정 테스트 케이스로 관리할 가치가 있습니다. 처음부터 모든 변형을 다룰 필요는 없습니다. 각 클래스에 대해 대표 페이로드 하나부터 CI에 추가하세요.

자동화된 API 취약점 탐지 도구로 나중에 범위를 확장할 수 있지만, 먼저 명백한 허점을 잡는 것이 중요합니다.

  • SQL 인젝션: 쿼리에 도달할 수 있는 필드에 1); DROP TABLE datasets;--를 보냅니다. 엔드포인트는 이를 리터럴 값으로 처리해 400을 반환하거나 빈 결과를 반환해야 하며, 데이터베이스 오류를 노출해서는 안 됩니다.
  • 템플릿 인젝션: 이름이나 레이블 필드에 {{ 7*7 }}, {{ config.__class__ }}를 보냅니다. 응답에 49가 포함된다면 템플릿 엔진이 입력을 평가한 것입니다. 이는 원격 코드 실행으로 이어질 수 있습니다.
  • 안전하지 않은 역직렬화 및 원격 코드 로더: 일반 값이 와야 할 위치에 pickle:// 로더나 직렬화된 객체를 보냅니다. 엔드포인트는 알 수 없는 로더를 화이트리스트 방식으로 거부해야 하며, 자동으로 처리하려 해서는 안 됩니다.
  • 명령 인젝션: 파일명이나 변환 옵션처럼 셸 인수가 될 수 있는 필드에 ; id, $(id)를 보냅니다. 사용자 ID가 포함된 200 응답은 단순한 이상 징후가 아니라 심각한 취약점 신호입니다.

오버사이즈, 잘못된 형식, 그리고 콘텐츠 유형 혼동

모든 적대적 입력이 영리한 문자열인 것은 아닙니다. 일부는 단순히 너무 크거나 형식이 잘못되어 있으며, 이런 입력은 유효성 검사 로직보다 먼저 파서를 실패시킬 수 있습니다.

오버사이즈 페이로드 테스트

다음 입력을 전송해 보세요.

  • 단일 필드에 5MB 크기의 동일한 문자
  • 백만 개 요소를 포함한 JSON 배열
  • 지나치게 깊게 중첩된 JSON 객체

건전한 API는 메모리를 계속 할당하는 대신 본문 크기 제한을 적용하고 413 Payload Too Large를 반환해야 합니다.

payload = {"name": "A" * 5_000_000}

response = httpx.post(
    "https://staging.internal/v1/datasets",
    json=payload,
    timeout=10,
)

assert response.status_code == 413
Enter fullscreen mode Exit fullscreen mode

잘못된 형식의 본문 테스트

다음과 같은 본문도 테스트하세요.

{"loader": "csv",
Enter fullscreen mode Exit fullscreen mode
{"loader": "csv",}
Enter fullscreen mode Exit fullscreen mode

이 경우 서버는 워커가 멈추거나 예외를 노출하는 대신 빠르게 400 Bad Request를 반환해야 합니다.

콘텐츠 유형 혼동 테스트

콘텐츠 유형과 실제 본문이 일치하는지도 확인해야 합니다.

선언한 Content-Type 실제 본문 기대 결과
application/json XML 400 또는 415
application/xml 외부 엔터티가 포함된 XML 안전한 거부
text/plain JSON 느슨한 파서가 JSON으로 처리하지 않는지 확인

각 불일치는 서버가 헤더만 신뢰하는지, 본문만 신뢰하는지, 또는 둘의 일치를 확인하는지 검증합니다. 파싱하기 전에 콘텐츠 유형과 본문 형식의 일치를 요구해야 합니다.

AI 에이전트가 판돈을 높이는 이유

여기서 설명한 원칙은 AI 에이전트 이전에도 유효했습니다. AI 에이전트는 공격의 양과 속도를 바꿉니다.

인간 공격자는 보통 한 번에 하나의 적대적 요청을 보냅니다. AI 에이전트는 머신 속도로 페이로드를 생성하고 전달하며, 사람이 직접 시도하지 않을 입력까지 조합할 수 있습니다.

특히 다음 세 가지 속성이 위험을 키웁니다.

  1. 입력 합성: 사람이 작성하지 않았고 테스트가 예상하지 못한 필드 값을 생성합니다.
  2. 재시도와 체이닝: 하나의 오염된 상위 문서가 몇 초 안에 수천 개의 적대적 API 요청으로 이어질 수 있습니다.
  3. 신뢰 경계 통과: 에이전트는 신뢰하도록 지시받은 데이터셋, 웹훅, 문서의 내용을 실제 API 요청으로 전달할 수 있습니다.

Hugging Face 사례의 "이 데이터 로드이 코드 실행으로 변하는" 패턴은 에이전트가 인지하지 못한 채 신뢰 경계를 넘어 전달할 수 있는 지시의 유형과 일치합니다.

API 팀을 위한 프롬프트 인젝션 글에서는 이 전달 과정과 위험을 더 자세히 다룹니다.

방어 원칙은 변하지 않습니다. 다만 에이전트 트래픽을 수동으로 검토할 수 없으므로 테스트와 검증을 자동화해야 합니다.

부정 테스트 스위트를 구축하고 모든 변경사항에 대해 CI에서 실행하기

위 사례를 모든 풀 리퀘스트에서 실행되는 스위트로 만드세요. 다음은 스테이징 엔드포인트를 호출하고 통제된 거부를 검증하는 pytest 예시입니다.

import httpx
import pytest

BASE = "https://staging.internal/v1"

HOSTILE_CONFIGS = [
    {"loader": "pickle://s3/models/payload.pkl", "format": "auto"},  # 원격 코드 로더
    {"loader": "csv", "name": "{{ 7*7 }}"},                          # 템플릿 인젝션
    {"loader": "csv", "name": "{{ config.__class__ }}"},             # 객체 순회
    {"loader": "csv", "filter": "1); DROP TABLE datasets;--"},       # SQL 인젝션
    {"loader": "csv", "name": "A" * 5_000_000},                      # 오버사이즈 필드
]

@pytest.mark.parametrize("config", HOSTILE_CONFIGS)
def test_dataset_config_is_refused(config):
    response = httpx.post(
        f"{BASE}/datasets",
        json={"config": config},
        timeout=10,
    )

    assert response.status_code in (400, 413, 422), response.text
    assert response.status_code < 500, (
        "5xx는 페이로드가 도달해서는 안 되는 로직에 도달했음을 의미합니다."
    )
    assert "49" not in response.text, (
        "템플릿이 렌더링되었습니다: 서버 측 템플릿 인젝션 가능성"
    )
Enter fullscreen mode Exit fullscreen mode

이 테스트를 CI에 연결해 병합을 제어하세요. 최소한의 GitHub Actions 작업은 다음과 같습니다.

name: api-abuse-tests

on: [push, pull_request]

jobs:
  negative-input:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pytest tests/negative_input.py -q
Enter fullscreen mode Exit fullscreen mode

스키마 우선(schema-first) 도구는 이 단계에서 유용합니다. Apidog에서는 OpenAPI 계약을 기반으로 엔드포인트를 설계할 수 있고, 테스트 중 모든 요청과 응답을 계약에 따라 검사할 수 있습니다.

해피 패스 시나리오 옆에 다음과 같은 부정 시나리오를 저장하세요.

  • 오버사이즈 필드
  • 잘못된 유형
  • 예상 외 속성
  • SQL, 템플릿, 명령 인젝션 문자열
  • 손상된 JSON
  • 콘텐츠 유형 불일치

각 시나리오에는 4xx 응답을 확인하는 어설션을 포함합니다. 이후 CLI를 통해 CI에서 같은 시나리오를 실행하면, 유효성 검사를 조용히 완화하는 변경사항 대신 빌드가 실패하게 만들 수 있습니다.

직접 적용하려면 Apidog을 다운로드한 뒤, 이미 보유한 엔드포인트에 부정 시나리오 하나부터 추가해 보세요.

경계도 분명히 해야 합니다. Apidog은 설계, 테스트, 목업, 문서화 도구입니다. 웹 애플리케이션 방화벽을 실행하거나 실시간 트래픽을 필터링하거나 SIEM을 대체하지는 않습니다. 테스트 중 계약 검증도 모든 익스플로잇을 잡아내지는 못합니다.

도구와 테스트가 제공하는 가치는 계약을 명시하고, 엔드포인트가 실제로 무엇을 허용하는지 정직하게 유지하는 데 있습니다. 그러면 프로덕션에서 "우리가 확인하지 않았던 입력" 때문에 놀랄 가능성을 줄일 수 있습니다.

자주 묻는 질문

  • 부정 테스트와 퍼징의 차이점은 무엇인가요?

    부정 테스트는 의도적으로 선택한 불량 입력을, 실패 클래스당 하나씩 보냅니다. 퍼징은 생각하지 못한 경우를 찾기 위해 대량의 무작위 또는 변형 입력을 보냅니다. 부정 테스트는 빠르고 결정론적이며 CI에서 실행하기 쉬우므로 먼저 시작하기 좋습니다. 그다음 예상 범위를 넘어서는 입력을 찾기 위해 퍼징을 추가하세요.

  • 이 테스트들을 프로덕션에 대해 실행해야 하나요?

    아니요. 스테이징 또는 격리된 환경에서 실행하세요. 오버사이즈 페이로드와 명령 인젝션 탐침은 시스템에 스트레스를 주도록 설계됐으며, 버그가 있다면 데이터를 변경할 수도 있습니다. 전용 테스트 환경에서 공격적으로 검증하세요.

  • 방화벽이나 WAF가 어쨌든 잡아주지 않나요?

    WAF는 유용한 심층 방어 계층이지만, 애플리케이션이 잘못된 입력을 거부하는 것을 대체하지는 않습니다. 규칙은 우회될 수 있고 WAF는 비즈니스 로직을 알 수 없습니다. 이 테스트의 목적은 엔드포인트 자체가 "안 돼"라고 말하는지 증명하는 것입니다.

  • 엔드포인트당 부정 케이스는 몇 개가 충분한가요?

    각 필드가 겪을 수 있는 실패 클래스당 하나를 목표로 하세요. 잘못된 유형, 범위 초과, 과도한 길이, 금지된 필드, 그리고 해당 형식에 맞는 인젝션 문자열을 포함하면 됩니다. 단순 개수보다 실패 클래스의 커버리지가 중요합니다.

  • 스키마 유효성 검사가 인젝션을 완전히 막아주나요?

    아닙니다. 엄격한 스키마는 잘못된 형식의 입력과 예상 외 필드를 상당수 차단하지만, 스키마를 통과한 값도 SQL 또는 템플릿 인젝션이 될 수 있습니다. 매개변수화된 쿼리, 안전한 역직렬화, 출력 인코딩을 유지하고, 스키마로 이 계층들이 방어해야 할 입력 표면적을 줄이세요.

Top comments (0)