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

입력은 공격 표면입니다. 주입, 과대, 비정상 형식의 페이로드에 대한 부정 테스트를 구축하고, 스키마를 보안 제어로 삼아 CI에서 실행하세요.

Ashley Innocent

Ashley Innocent

23 July 2026

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

Apidog 엔터프라이즈

온프레미스 배포

SSO & RBAC

SOC 2 준수

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

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

2026년 7월, Hugging Face는 도난당한 비밀번호가 아닌 데이터가 진입 벡터였던 보안 사고를 설명했습니다. 저희는 해당 침해로부터 얻은 교훈을 별도로 다루었으며, 이 가이드는 실질적인 후반부입니다. 이 가이드에서는 공격자가 보내는 종류의 입력을 보내는 테스트를 구축하고, 모든 변경사항에 대해 자동으로 실행합니다. 이 카테고리들은 OWASP API 보안 톱 10과 일치하며, 이는 새 탭에 열어둘 가치가 있습니다. Apidog은 이러한 테스트를 위한 계약을 설계하고 구동하는 한 가지 방법이지만, 이 아이디어는 이미 사용 중인 어떤 프레임워크에도 적용됩니다.

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

유효성 검사는 종종 사용자 경험상의 예의로 취급됩니다: 빈 이메일을 잡고, 빨간색 테두리를 보여주고, 넘어가는 식입니다. 이러한 접근 방식이 문제입니다. API가 허용하는 모든 필드는 호출자가 깰 수 있는 약속이며, 깨진 약속 하나하나는 당신의 로직으로 향하는 경로가 됩니다. 작은 정수일 것으로 예상했던 `limit` 매개변수는 `999999999`가 됩니다. 단어 하나일 것으로 예상했던 `filename`은 `../../etc/passwd`가 됩니다. 설정을 담을 것으로 예상했던 `config` 객체는 일련의 명령이 됩니다.

보안 테스트는 마지막에 덧붙여지는 별도의 규율이 아닙니다. 그것은 이미 알고 있는 것과 동일한 부정 테스트이며, 당신에게 가장 해를 끼칠 가능성이 있는 필드를 목표로 합니다. "이 필드에 들어갈 수 있는 최악의 것은 무엇인가?"라는 질문을 하는 습관을 들이면, 저희 API 보안 모범 사례 가이드에 있는 대부분의 관행을 따르게 됩니다. 이 기사의 나머지 부분은 그 하나의 질문을 실행 가능한 구체적인 테스트로 전환합니다.

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

Hugging Face 사고는 입력이 왜 이러한 주의를 기울여야 하는지에 대한 명확한 예시입니다. Hugging Face는 진입 벡터가 악성 데이터셋이었다고 밝혔습니다. 조작된 데이터셋이 원격 코드 데이터셋 로더를 트리거했고, 데이터셋 구성 내부에 템플릿 인젝션이 존재했습니다. 회사의 공식 설명은 보안 사고 보고서에서 확인할 수 있습니다.

그 실패의 형태를 살펴보세요. 엔드포인트는 데이터로 설명된 어떤 것을 수락했습니다. 해당 데이터를 로드하는 것은 공격자가 제어하는 ​​명령을 실행할 수 있는 코드 경로를 실행했습니다. "이 데이터 로드"가 "이 코드 실행"이 된 것입니다. 템플릿 인젝션은 더 작은 규모에서 동일한 이야기입니다. 비활성 텍스트여야 할 구성 값이 평가되어, 텍스트가 실행으로 변한 것입니다.

핵심은 "Hugging Face가 드문 실수를 저질렀다"는 것이 아닙니다. 그것은 로더 이름, 형식, 템플릿, 직렬화된 객체 또는 구성 블롭을 허용하는 모든 엔드포인트가 의도했든 아니든 명령을 허용하고 있다는 것입니다. 만약 적대적인 구성을 해당 엔드포인트로 보내는 테스트를 작성하지 않았다면, 그것이 비활성 상태를 유지한다는 가정을 실제로 확인하지 않은 것입니다. 테스트되지 않은 그 가정이 전체 취약점입니다.

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

추가할 수 있는 가장 저렴한 통제는 엣지에서의 엄격한 스키마입니다. 스키마는 단순히 문서화만을 위한 것이 아닙니다. 일치하지 않는 모든 것을 거부할 때, 스키마는 비즈니스 로직이 요청을 보기 전에 실행되는 필터가 됩니다. JSON 스키마는 그 필터를 엄격하게 만드는 기본 요소를 제공합니다.

다음은 스토리에 나오는 데이터셋 구성에 대한 스키마이며, 대부분의 적대적 입력이 애플리케이션 코드에 도달하지 못하도록 작성되었습니다.

{
  "$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 }
  }
}

이것을 네 가지 개별 방어로 해석하세요. `additionalProperties: false`는 숨겨진 `template` 필드를 즉시 거부하므로 공격자는 이를 추가할 수 없습니다. `loader` 열거형은 `pickle://` 또는 다른 원격 코드 로더가 유효한 값이 아님을 의미합니다. `maxLength`는 메모리 고갈을 목표로 하는 수 메가바이트 크기의 문자열을 차단합니다. `name`의 `pattern`은 `{{` 및 `'; DROP TABLE` 문자가 더 이상 진행되기 전에 거부합니다. 이 코드 라인 중 어느 것도 공격자에 대해 알지 못합니다. 이들은 실제로 지원하는 좁은 범위의 입력만 허용하며, 그 좁은 범위 자체가 보안 속성입니다.

이와 같은 계약 유효성 검사가 모든 익스플로잇을 잡아내지는 못하며, 어떤 스키마도 그럴 수 없습니다. 이 방식이 막는 것은 특정하고 흔한 범주, 즉 "이 엔드포인트가 무엇을 허용하는지 전혀 확인하지 않았다"는 버그입니다. 놀랍게도 많은 침해가 이 범주에서 시작됩니다.

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

해피 패스 테스트는 좋은 입력이 좋은 출력을 생성하는지 확인합니다. 부정 테스트는 잘못된 입력이 통제된 거부를 생성하는지 확인합니다. 이 구분이 중요한 이유는 거부가 기능이기 때문입니다. 명확한 오류 메시지가 있는 400 응답은 API가 자신의 경계를 방어하고 있음을 의미합니다. 500 응답은 API가 통제력을 잃었음을 의미합니다.

부정 케이스를 항상 같은 방식으로 구축하세요. 각 필드에 대해 거부해야 하는 것들을 적으세요: 잘못된 유형, 필수 필드의 누락, 금지된 필드의 존재, 너무 긴 값, 범위를 벗어난 값, 그리고 형식에 맞는 인젝션 문자열 등이 있습니다. 그런 다음 응답에 대해 두 가지를 확인하세요. 첫째, 상태 코드가 4xx여야 하며, 일반적으로 400 또는 422입니다. 둘째, 상태 코드가 절대로 5xx가 아니어야 합니다. 500은 적대적인 입력이 이를 처리할 준비가 되지 않은 코드에 도달했음을 의미하며, 이는 공격자가 원하는 접근성입니다. 저희 API 보안 테스트 체크리스트에는 적응할 수 있는 필드별 시작 목록이 있습니다.

한 가지 규칙이 이를 정직하게 유지합니다. 오류 텍스트가 아닌 동작에 대해 어설션하세요. 메시지가 "invalid loader"라고 읽히는지 어설션한다면, 무해한 리팩토링으로 인해 테스트가 깨지고 팀은 이를 완화하는 법을 배우게 됩니다. 상태 코드를 어설션하고, 가능한 경우, 아무런 부작용도 발생하지 않았음을 어설션하세요.

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

몇몇 인젝션 계열은 매우 자주 나타나므로, 일회성 수동 검사가 아닌 고정된 테스트 케이스를 가질 가치가 있습니다. 여기서 모든 경우를 다 다룰 필요는 없습니다. 각 클래스당 하나의 탐색 케이스만 있으면 회귀 발생 시 명확하게 실패를 알 수 있습니다. 자동화된 API 취약점 탐지 도구는 나중에 커버리지를 확장할 수 있지만, 소수의 수동 작성 케이스로도 명백한 허점들을 먼저 잡을 수 있습니다.

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

모든 적대적 입력이 영리한 문자열인 것은 아닙니다. 일부는 단순히 너무 크거나 형식이 잘못되어 있으며, 이러한 입력은 유효성 검사 로직이 실행되기도 전에 파서를 깨뜨리는 경우가 많습니다.

오버사이즈 페이로드를 보내세요: 단일 필드에 5메가바이트의 한 문자만 담거나, 백만 개의 요소를 가진 JSON 배열을 보내세요. 건전한 API는 본문 크기 제한을 적용하고, 메모리가 소진될 때까지 할당하는 대신 413을 반환합니다. 잘못된 형식의 본문도 보내세요: 잘린 JSON, 후행 쉼표, 또는 스택 고갈을 탐색하기 위해 천 단계 깊이로 중첩된 JSON 등입니다. 올바른 응답은 멈춘 워커가 아닌 빠른 400입니다.

콘텐츠 유형 혼동은 조용한 문제입니다. `Content-Type: application/json`으로 선언했지만 XML을 보내거나, `application/xml`로 선언하고 외부 엔티티를 포함한 페이로드를 보내 XXE를 탐색해보세요. 반대로 JSON을 `text/plain`으로 보내 느슨한 파서가 여전히 이를 수락하는지 확인해 보세요. 각 불일치는 서버가 헤더를 신뢰하는지, 본문을 신뢰하는지, 아니면 이 둘이 일치하는지 확인하는지 테스트합니다. 어떤 것을 파싱하기 전에 일치 여부를 요구해야 합니다.

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

위에 언급된 모든 내용은 에이전트가 존재하기 전부터 사실이었습니다. 에이전트는 공격의 양과 속도를 변화시킵니다. 인간 공격자는 한 번에 하나의 적대적 요청을 입력합니다. AI 에이전트는 머신 속도로 페이로드를 생성하고 전달하며, 사람이 시도조차 하지 않을 입력들을 기꺼이 구성할 것입니다.

세 가지 속성이 이 문제를 더욱 악화시킵니다. 에이전트는 입력을 합성하므로, 사람이 작성하지 않았고 어떤 테스트도 예상하지 못했던 필드 값을 생성합니다. 에이전트는 호출을 재시도하고 연결하므로, 하나의 오염된 상위 문서가 몇 초 안에 엔드포인트에 대한 수천 개의 적대적 요청으로 변할 수 있습니다. 그리고 에이전트는 신뢰하도록 지시받은 데이터를 전달하는데, 이것이 데이터셋이나 웹훅에 숨겨진 페이로드가 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__ }}"},             # 객체 순회 (object traversal)
    {"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):
    r = httpx.post(f"{BASE}/datasets", json={"config": config}, timeout=10)
    assert r.status_code in (400, 413, 422), r.text  # 거부하는 경계
    assert r.status_code < 500, "5xx는 페이로드가 도달해서는 안 되는 로직에 도달했음을 의미합니다."
    assert "49" not in r.text, "템플릿 렌더링됨: 서버 측 템플릿 인젝션"

이를 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

이것이 스키마 우선(schema-first) 도구가 그 역할을 하는 지점입니다. Apidog에서는 OpenAPI 계약에 따라 엔드포인트를 설계하므로, 테스트하는 동안 모든 요청과 응답이 해당 계약에 대해 검사됩니다. 오버사이즈 필드, 잘못된 유형, 위에서 언급한 인젝션 문자열과 같은 부정적인 시나리오를 해피 패스 시나리오 옆에 저장할 수 있으며, 각각 상태 코드가 4xx임을 확인하는 어설션을 포함합니다. 그런 다음 Apidog CLI를 통해 CI에서 동일한 시나리오를 실행하면, 유효성 검사를 조용히 완화하는 변경사항이 배포되는 대신 빌드가 실패합니다. 직접 사용해보고 싶다면, Apidog을 다운로드하여 이미 가지고 있는 엔드포인트에 부정 시나리오 하나를 추가해 보세요.

경계를 명확히 하세요. Apidog은 설계, 테스트, 목업 및 문서화 도구입니다. 웹 애플리케이션 방화벽을 실행하거나 실시간 트래픽을 필터링하거나 SIEM을 대체하지 않으며, 테스트 중 계약 유효성 검사가 모든 익스플로잇을 잡지는 못할 것입니다. Apidog이 잘하는 것은 계약을 명시하고 엔드포인트가 무엇을 허용하는지에 대해 정직하게 유지함으로써, 프로덕션에서 "우리가 확인하지 않았던" 범주의 문제로 인해 놀라는 일이 없도록 하는 것입니다.

자주 묻는 질문

Apidog에서 API 설계-첫 번째 연습

API를 더 쉽게 구축하고 사용하는 방법을 발견하세요