DEV Community

Cover image for How to Test Your API Against Untrusted Input (Before an Agent Does)
Hassann
Hassann

Posted on • Originally published at apidog.com

How to Test Your API Against Untrusted Input (Before an Agent Does)

TL;DR: Your API’s input is an attack surface, so test it like one. Write negative cases that send oversized fields, wrong types, malformed bodies, and injection strings, then assert the endpoint answers a 4xx and never a 5xx. Turn schema validation into a security control with additionalProperties: false, enums, and length limits. Run the whole suite in CI on every change. AI agents make this urgent: they generate and forward payloads at machine speed, so “load this data” quietly becoming “run this code” now scales.

Most test suites prove an API works when the caller is polite: send a valid body, get a 200, pass the assertion. That says little about hostile input. Treat every request body, query string, header, file upload, webhook payload, and AI-generated JSON as untrusted. Your endpoint should assume that someone will eventually send the worst possible version of every accepted value.

Try Apidog today

In July 2026, Hugging Face described a security incident whose entry vector was data rather than a stolen password. We covered the lessons from that breach separately; this guide focuses on implementation. You will build tests that send attacker-style input and run them automatically on every change. The categories align with the OWASP API Security Top 10. Apidog is one way to define the contract and run these tests, but the approach works with any framework or test stack.

Input is an attack surface, not a form field

Validation is often treated as a UX concern: catch an empty email, show an error, move on. For APIs, validation is also a security boundary.

Every accepted field is a promise the caller can break:

  • A limit expected to be a small integer becomes 999999999.
  • A filename expected to be a single name becomes ../../etc/passwd.
  • A config object expected to contain settings becomes a set of instructions.
  • A JSON field expected to be short text becomes a multi-megabyte payload.

Security testing is negative testing applied to inputs that can affect execution, storage, parsing, or resource use. For each field, ask:

What is the worst thing that can fit here?

That question drives the practices in our API security best practices guide and the tests in this article.

How “load this data” became “run this code”

The Hugging Face incident shows why input boundaries matter. Hugging Face said the entry vector was malicious datasets: a crafted dataset triggered a remote-code dataset loader, and a template injection existed in a dataset configuration. Read the company’s account in its security incident report.

The failure shape is important:

  1. An endpoint accepted something described as data.
  2. Loading that data entered a code path capable of executing attacker-controlled instructions.
  3. A configuration value that should have been inert text was evaluated.

In other words, “load this data” became “run this code.”

Any endpoint that accepts a loader name, format, template, serialized object, or configuration blob may be accepting instructions—even if that was not the intent. If you have not tested hostile values against that endpoint, you have not verified that those values remain inert.

Schema validation as a security control

Add strict validation at the API boundary. A schema is not just documentation: when enforced, it filters requests before business logic handles them.

JSON Schema provides the primitives you need:

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

This schema provides several independent controls:

Schema rule What it blocks
additionalProperties: false Smuggled fields such as template
loader enum Unsupported or remote-code loader values such as pickle://
maxLength Oversized strings intended to consume memory or processing time
pattern Characters outside the supported naming format, including values such as {{

The goal is not to identify every malicious payload. The goal is to accept only the inputs your endpoint explicitly supports.

Schema validation will not stop every exploit. A value can be schema-valid and still be dangerous when passed to a SQL query, template engine, shell command, or deserializer. But strict contracts close a common failure mode: accepting data that the endpoint was never designed to handle.

Negative testing: prove the endpoint says no

Happy-path tests verify that valid input succeeds. Negative tests verify that invalid input fails safely and predictably.

For each field, define cases it must reject:

  • Wrong type
  • Missing required value
  • Forbidden extra field
  • Value that is too long
  • Value outside the allowed range
  • Invalid format
  • Injection payloads relevant to how the value is used

For every hostile request, assert two outcomes:

  1. The response is a controlled 4xx, typically 400, 413, or 422.
  2. The response is never a 5xx.

A 500 means hostile input reached code that did not handle it safely. That is exactly the path an attacker wants to find.

Use behavior-focused assertions. Avoid tying tests to exact error messages unless the message is part of your public API contract. Prefer status checks and side-effect checks:

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

Use our API security testing checklist as a field-by-field starting point.

Injection classes worth dedicated tests

Maintain a small set of permanent tests for common injection families. You do not need exhaustive payload lists on day one. One representative probe per class is enough to catch obvious regressions.

SQL injection

Send a payload such as:

1); DROP TABLE datasets;--
Enter fullscreen mode Exit fullscreen mode

Use it in fields that could influence a database query. The endpoint should reject the value, treat it as literal data, or return no result. It must never expose a database error.

Template injection

Send values such as:

{{ 7*7 }}
{{ config.__class__ }}
Enter fullscreen mode Exit fullscreen mode

Test fields that may reach templates, labels, generated documents, or notification content. If a response contains 49, your input was evaluated rather than treated as text.

Insecure deserialization and remote-code loaders

Send an unsupported loader:

{
  "loader": "pickle://s3/models/payload.pkl"
}
Enter fullscreen mode Exit fullscreen mode

Or send a serialized object where a plain value is expected. Reject unknown loaders using an allowlist. Do not attempt to infer or “helpfully” process unrecognized formats.

Command injection

For any value that may become a shell argument—such as a filename, conversion option, or export setting—send:

; id
$(id)
Enter fullscreen mode Exit fullscreen mode

A response that reveals command output is a critical finding. Avoid shell execution where possible; otherwise, use safe argument APIs and strict allowlists.

Tools for automated API vulnerability detection can expand coverage later, but hand-written regression cases should come first.

Test oversized, malformed, and mismatched bodies

Hostile input is not always a clever string. Large and malformed payloads often break parsers before application validation runs.

Add tests for:

  • A string field containing five megabytes of one character
  • A JSON array containing one million elements
  • Truncated JSON
  • JSON with trailing commas
  • Excessively nested JSON
  • Invalid request encodings

A healthy API should enforce body-size limits and fail quickly:

  • Return 413 Payload Too Large for oversized requests.
  • Return 400 Bad Request for malformed request bodies.
  • Avoid hung workers, memory exhaustion, and parser crashes.

Also test content-type handling. For example:

Content-Type: application/json
Enter fullscreen mode Exit fullscreen mode

with an XML body, or:

Content-Type: text/plain
Enter fullscreen mode Exit fullscreen mode

with a JSON body.

Test XML handling carefully, including payloads with external entities, to probe for XXE. Your server should require the declared content type and actual body format to agree before parsing.

Why AI agents raise the stakes

These input risks existed before AI agents. Agents increase the volume and speed of unsafe input.

Three properties make this more urgent:

  1. Agents synthesize input. They produce values no human explicitly wrote and no existing test may cover.
  2. Agents retry and chain calls. One poisoned document can cause thousands of requests in seconds.
  3. Agents forward trusted-looking data. A payload hidden in a dataset, webhook, or document can become a real API request across a trust boundary.

The Hugging Face pattern—where loading data becomes code execution—is exactly the type of instruction an agent can carry forward without recognizing the danger. Our guide to prompt injection for API teams covers that hand-off in more detail.

The defense remains the same: validate inputs, reject unsupported values, and run the checks automatically. Manual review cannot keep up with machine-generated traffic.

Build the negative suite and run it in CI

Start with a parameterized test suite that sends representative hostile configurations to a staging or isolated environment.

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;--"},
    {"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 means the payload reached logic it should not"
    )
    assert "49" not in response.text, (
        "Template rendered: possible server-side template injection"
    )
Enter fullscreen mode Exit fullscreen mode

Add tests for malformed and content-type-confusion cases separately:

def test_malformed_json_is_rejected():
    response = httpx.post(
        f"{BASE}/datasets",
        content=b'{"config":',
        headers={"Content-Type": "application/json"},
        timeout=10,
    )

    assert response.status_code == 400
    assert response.status_code < 500


def test_json_sent_as_plain_text_is_rejected():
    response = httpx.post(
        f"{BASE}/datasets",
        content=b'{"config":{"loader":"csv","name":"example"}}',
        headers={"Content-Type": "text/plain"},
        timeout=10,
    )

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

Then gate pull requests with CI:

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

Run these tests against staging or a dedicated isolated environment—not production. Some cases intentionally stress parsers, memory limits, and error paths. Others could mutate data if a vulnerability exists.

Use contract testing to keep validation from drifting

A schema-first workflow makes it easier to keep validation and tests aligned. In Apidog, define an endpoint from an OpenAPI contract and validate requests and responses against that contract during testing.

Save negative scenarios beside happy-path requests:

  • Oversized fields
  • Wrong types
  • Unsupported enum values
  • Unexpected properties
  • SQL injection strings
  • Template injection strings
  • Malformed request bodies

For each scenario, assert a 4xx response. Run the same scenarios in CI through the Apidog CLI so a change that loosens validation fails the build instead of reaching production.

To get started, Download Apidog and add one negative scenario to an endpoint you already maintain.

Be clear about the boundary: Apidog is a design, test, mock, and documentation tool. It does not run a web application firewall, filter live traffic, or replace a SIEM. Contract validation also does not catch every exploit. Its value is making accepted input explicit and continuously testing that your endpoint enforces that contract.

Frequently asked questions

What is the difference between negative testing and fuzzing?

Negative testing uses a curated set of intentionally bad inputs, usually one or more cases per known failure class. Fuzzing uses many random or mutated inputs to discover cases you did not anticipate.

Start with negative tests because they are deterministic, fast, and easy to run in CI. Add fuzzing when you need broader exploration.

Should these tests run against production?

No. Use staging or an isolated environment. Oversized payloads, malformed bodies, and command-injection probes are designed to stress the system. If a bug exists, some tests could also mutate data.

Won’t a firewall or WAF catch this?

A WAF is useful defense in depth, but it is not a replacement for application-level validation. WAF rules can be bypassed and cannot fully understand your business logic. Your API should reject invalid input even without an upstream filter.

How many negative cases are enough per endpoint?

Aim for at least one case per field and failure class:

  • Wrong type
  • Missing required value
  • Out-of-range value
  • Oversized value
  • Forbidden field
  • Invalid format
  • Relevant injection payload

That is usually a handful of tests per endpoint. Coverage of failure classes matters more than raw test count.

Does schema validation stop injection completely?

No. Strict schemas block many malformed, oversized, and unexpected values, but schema-valid values can still be dangerous. Keep parameterized queries, safe deserialization, output encoding, and safe command execution practices in place.

Use the schema to shrink the input surface. Use negative tests to prove the boundary continues to hold.

Top comments (0)