Building a Hostile Site to Test Our AI

QualityMax crawls websites. Users give us a URL and instructions, and our AI navigates the site, understands its structure, and generates Playwright tests. That means our crawler visits pages it has never seen before — pages controlled by strangers.

What happens when one of those pages is actively trying to attack you?

To find out, we built qamax-adversary-site — an intentionally hostile website designed to break, confuse, and exploit our AI crawl pipeline. Here's what's in it and what we learned.

Why Attack Yourself?

When you build a system that processes untrusted input, you test it with untrusted input. For a web crawler backed by an LLM, the "input" isn't just form fields — it's the entire page. HTML, JavaScript, CSS, meta tags, comments, hidden elements, HTTP headers. All of it is potentially adversarial.

We needed a controlled environment where every known attack vector was present, repeatable, and measurable. A red-team site we could run our pipeline against after every change and know exactly what should and shouldn't get through.

Prompt Injection

The most interesting attack surface for an AI-powered crawler is prompt injection. If the AI reads page content and uses it to generate code, a malicious page can try to hijack the AI's instructions.

Our adversary site includes multiple prompt injection vectors:

The key insight: prompt injection against a crawler is different from injection against a chatbot. The attacker doesn't need to extract secrets — they just need the AI to generate malicious test code or skip critical assertions.

XSS and Script Traps

Even though our crawler runs in a sandboxed environment, we test against script-based attacks to ensure defense in depth:

Our Playwright execution environment runs with JavaScript enabled (it has to — most modern sites require it), so the sandbox boundary and process isolation are what protect us, not JS blocking.

Redirect Loops and Resource Exhaustion

Simple but effective: what happens when a page tries to waste your resources?

What We Learned

Running our pipeline against the adversary site after every significant change has caught real issues. Here are the most impactful lessons:

Prompt Isolation Is Non-Negotiable

Page content must be clearly separated from system instructions in the prompt. We use explicit delimiters and instruct the model to treat everything between them as untrusted user content. After we strengthened this boundary, prompt injection success rates dropped from "occasionally worked" to "never succeeded" in our test suite.

Timeouts Need to Be Aggressive

Our initial timeout of 120 seconds was too generous. Slow responses and redirect loops wasted resources. We now enforce strict per-page timeouts (30 seconds for navigation, 60 seconds total per test) and redirect limits (maximum 5 hops).

Sandbox Everything

The Playwright browser runs in a sandboxed process with no network access to internal services, no filesystem access, and hard memory limits. Even if injected code runs inside the browser, it can't reach anything valuable. This is defense in depth: assume the browser is compromised and limit what a compromised browser can do.

Content Size Limits Matter

Before sending page content to the LLM, we truncate it to a sensible maximum. A 10MB HTML page shouldn't generate a 10MB prompt. We also strip non-visible content (scripts, style blocks, hidden elements) before the AI sees the page, which has the side effect of removing many prompt injection vectors.

Test the Failure Modes

The adversary site doesn't just test whether attacks succeed. It tests whether failures are handled gracefully. A redirect loop should produce a clear error message, not a hung process. A massive page should be rejected with a log entry, not silently truncated. Every failure mode is a test case.

It's Open Source

The adversary site is public: github.com/Quality-Max/qamax-adversary-site. If you're building any system that processes untrusted web content — especially one backed by an LLM — feel free to fork it and add your own attack vectors. We accept pull requests.

Security isn't a feature you ship once. It's a practice you maintain. The adversary site is how we maintain ours.