← All news
resilience5 min read

Your AI assistant won't remind you to validate inputs: here's why that hurts

By Seaworthy · 14 September 2026

A colourful collection of vintage postage stamps arranged in a pile

Photo by Mason @ TRI on Unsplash

Your AI assistant won't remind you to validate inputs: here's why that hurts

I've reviewed a lot of code written with AI assistants. The pattern is consistent: the logic works, the tests pass, and the security basics are missing. Input validation and request size limits are the two most common gaps. And I get it. When you're moving fast and an AI hands you a working endpoint, adding a check for Content-Length feels like busywork. But that busywork is the difference between a Tuesday afternoon and a Friday night incident.

Here's a simple example. A developer asks for a file upload endpoint. The AI produces something like this:

from flask import Flask, request

app = Flask(__name__)

@app.route('/upload', methods=['POST'])
def upload():
    file = request.files['file']
    file.save('/uploads/' + file.filename)
    return 'OK'

It runs. It works. It also accepts a 10 GB file, writes it to disk, and trusts the filename completely. An attacker can send ../../etc/passwd as the filename, or just flood the disk until the service dies. No validation, no size limit, no resilience.

The fix is not complicated. You add a maximum content length, you check the filename, and you reject anything that doesn't fit the expected shape. The AI won't do this by default because it optimizes for a working demo, not for production survival.

from flask import Flask, request, abort
import os

app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024  # 5 MB

ALLOWED_EXTENSIONS = {'.png', '.jpg', '.pdf'}

@app.route('/upload', methods=['POST'])
def upload():
    if 'file' not in request.files:
        abort(400, 'No file part')
    file = request.files['file']
    if file.filename == '':
        abort(400, 'No selected file')
    ext = os.path.splitext(file.filename)[1].lower()
    if ext not in ALLOWED_EXTENSIONS:
        abort(400, 'File type not allowed')
    safe_name = os.path.basename(file.filename)
    file.save(os.path.join('/uploads', safe_name))
    return 'OK'

That's it. Five lines of validation and one config setting. Yet I see production systems without them every week.

Why does this happen? Because AI assistants are trained on code that works, not code that survives. They don't know your threat model. They don't know that your service runs behind a load balancer that will happily pass a 2 GB request. They don't know that a single malformed JSON body can cause your parser to allocate gigabytes of memory. You have to bring that knowledge yourself.

The request size limit is the one people forget most often. Even if you validate every field, an attacker can send a body that is technically valid but enormous. A JSON parser that reads the whole thing into memory will die. A form parser that accepts unlimited fields will die. A file upload that streams to disk without a cap will fill the disk. The fix is almost always a single line in your framework or reverse proxy, but you have to remember to add it.

Consider a plain JSON endpoint that accepts a list of IDs and returns matching records. The AI writes a handler that calls request.get_json() and iterates over the result. If nobody caps the array length, an attacker sends a payload with ten million entries. The parser allocates memory for every one of them before your loop even starts. The process either gets killed by the OOM killer or stalls while the garbage collector thrashes. A single line near the top of the handler prevents all of it:

@app.route('/lookup', methods=['POST'])
def lookup():
    data = request.get_json(silent=True)
    if not isinstance(data, dict):
        abort(400, 'Expected a JSON object')
    ids = data.get('ids')
    if not isinstance(ids, list) or len(ids) > 100:
        abort(400, 'ids must be a list of at most 100 items')
    for item in ids:
        if not isinstance(item, str) or len(item) > 64:
            abort(400, 'Each id must be a short string')
    results = fetch_records(ids)
    return {'results': results}

Three checks, each a couple of lines. Now your endpoint rejects the hostile input before it touches memory, before it touches the database, and before it touches your logging pipeline. The AI will not write this for you, because nothing in its training signal rewards defensive code over working code.

The same logic applies to numeric fields, date ranges, pagination offsets, and nested objects. A page_size parameter of one million is legal JSON and legal HTTP. It is also a denial-of-service vector if you pass it straight to your ORM. A sort parameter that accepts arbitrary strings is legal JSON until someone passes a subquery. Every field the AI generates is a small contract, and every contract needs a boundary.

My reaction to this trend is not anger at the AI tools. They are useful. My reaction is frustration at how easy it is to let them lull you into skipping the boring parts. Security and operations basics are boring. They are also what keeps your service running when someone decides to test your limits. If you use an AI assistant, treat its output as a first draft. Then add the validation, the size limits, and the error handling. Every time.

One more thing: don't rely on client-side validation. The client is not your friend. The client is an attacker with a proxy. Always validate on the server. Always cap the request. Always handle the case where the input is missing, too large, or the wrong type. The AI won't remind you. That's your job.

If you want a checklist or a scanner that flags missing size limits in your codebase, Seaworthy offers a free tool at seaworthy.dev.

This article was generated by AI and summarises publicly available sources.

From the makers of Seaworthy

Seaworthy scans your repo for the issues covered in articles like this one.

Security gaps, exposed secrets, and misconfigurations — caught before you deploy. Free to run, no account needed.

$ npx seaworthycode