Work with me→

Exploit-verified AI vulnerability remediation

Find It, Prove It, Fix It — and Prove the Fix

An evidence-based case study in exploit-verified AI vulnerability remediation: the AI runs the whole loop on one real vulnerability, end to end — find, prove, fix, confirm, and the blast radius the fix reaches into — and refuses to certify a fix that doesn't hold.

Converging on one shared handler CoveredGap
DELETE · 10CREATE · 94UPDATE · 61SHARED HANDLER165 ENTITY-OPERATIONS → ONE HANDLER142 COVERED · 23 GAP
Every create, update and delete in the app funnels into one handler — the dot at the center. The one line the AI fixes lives there. Green is what a passing test would catch if the fix broke it; red is the gap.

Coverage is counted per operation — a test only counts when its latest run passed on that exact entity and operation.

create83 / 94 covered
update50 / 61 covered
delete9 / 10 covered
165
Impacted operations across 97 entities
5 / 5
Exploits blocked after the fix
86%
Of that surface has a passing test

For weeks the work followed me into sleep. Half-dreaming through a bug, muttering to myself — or to the AI — and then it lands and I'm awake: the core architecture is wrong, damn. 3am. And with it, the question underneath all of it: how do you expedite AI-augmented coding and cut time to market — without shipping the mess the AI leaves behind?

The answer isn't a better scanner. Finding bugs stopped being the hard part the moment an AI could list every broken check in an afternoon. The hard part is trust — earning enough of it to let AI change production code with no human in the loop. And every step that makes autonomous fixing possible creates the next risk you have to close. By hand, this whole lifecycle is weeks of security-plus-dev work per finding.

So read this as a ladder, not a pipeline. Each rung earns a little more trust and hands you the next problem — run end to end, by the platform, on one real vulnerability in a rental-property manager built on a no-code platform. Where a rung is still hardening, I say so.

Find→ Prove→ Fix→ Confirm→ Blast radius

1 Find it

Detection that can't hallucinate — every finding validated against real source

Four specialized scanners read the codebase in parallel, each tuned to one class of flaw. Every finding is checked back against the actual source before it persists — if the line doesn't say what the scanner claims, it's thrown out. What survives is anchored to a real location.

Information Exposure — raw database error leaked on entity create
CWE-200
src/components/tenants/TenantForm.jsx:27unit: TenantForm

The global exception handler returns the raw exception message to the client — table and constraint names, the failing SQL, and submitted field values, which include PII. A constraint-violating submission is enough to trigger it · 5 of 5 scenarios fired.

The finding is anchored on the form — the reachable entry point — but the leak happens elsewhere, and picking the exact snippet is the whole discipline. Two lines matter, in two different files.

The trigger — front-end
src/components/tenants/TenantForm.jsxTrigger
172const handleSubmit = async (e) => {
...  // name, email, phone, notes — the tenant's own PII
192  await onSubmit(submitData); // → Tenant.create, no request model
202};

The form ships arbitrary tenant fields straight to POST /api/rensure/entity/tenant with no schema, so a bad type or duplicate key becomes a database error.

The sink — where the data actually leaves the boundary
src/api/entity_router.py:195Sink · the leak
191    except HTTPException:
192        raise
193    except Exception as e:
194        lg.error(f"Error updating {entity_name}/{entity_id}: {e}")
195        raise HTTPException(status_code=500, detail=str(e))

That one line is the finding. str(exc) on a database error is the table name, the constraint, the failing SQL, and whatever the user submitted — handed back to the browser.

Opens →A validated finding still isn't a confirmed one. Scanners are noisy; most of what they raise is false positive. Nothing here earns a fix until something makes it actually fire.

2 Prove it

An exploit that actually fires — captured, not asserted

A static finding is a guess. So the platform writes real exploit code, runs it against the live app, and captures the raw HTTP exchange — then renders an independent verdict: did protected data come back, true or false. The trace pins the leak to one server-side step; the exploit targets it.

POST /api/rensure/entity/tenant · 11 steps
1front-endsrc/components/tenants/TenantForm.jsx
2front-endsrc/pages/Tenants.jsx
3front-endsrc/pages/Tenants.jsx
4front-endsrc/api/apiClient.js
5server-sidesrc/api/entity_router.py
6server-sidesrc/api/entity_router.py
7server-sidesrc/api/entity_router.py⚠ leak
8server-sidesrc/api/entity_router.py
9server-sidesrc/clients/rensure_api.py
10front-endsrc/api/apiClient.js
11front-endsrc/pages/Tenants.jsx

Here is the exchange it captured on the tenant record, unedited. The attack is one malformed field — a landlord_id that points at no user, forcing a foreign-key error the server doesn't handle.

POST /api/rensure/entity/tenantExploit request
{"first_name": "John", "last_name": "Doe", "email": "[email protected]", "phone": "5551234567", "status": "prospect", "notes": "Test tenant", "landlord_id": "00000000-0000-0000-0000-000000000000", "emergency_contact_name": "Jane Doe", "emergency_contact_phone": "5559876543"}
HTTP 500 · response body · returned to the browserWhat leaked
{"detail":"(psycopg2.errors.ForeignKeyViolation) insert or update on table \"tenant\" violates foreign key constraint \"tenant_landlord_id_fkey\"\nDETAIL:  Key (landlord_id)=(00000000-0000-0000-0000-000000000000) is not present in table \"user\".\n\n[SQL: INSERT INTO tenant (created_date, updated_date, created_by_id, created_by, is_sample, id, organization_id, landlord_id, user_id, first_name, last_name, email, phone, emergency_contact_name, emergency_contact_phone, status, notes, billing_email, currency, timesheet_period, logo_url, plan, user_limit, grace_period_start) VALUES (%(created_date)s, %(updated_date)s, %(created_by_id)s, %(created_by)s, %(is_sample)s, %(id)s, %(organization_id)s, %(landlord_id)s, %(user_id)s, %(first_name)s, %(last_name)s, %(email)s, %(phone)s, %(emergency_contact_name)s, %(emergency_contact_phone)s, %(status)s, %(notes)s, %(billing_email)s, %(currency)s, %(timesheet_period)s, %(logo_url)s, %(plan)s, %(user_limit)s, %(grace_period_start)s)]\n[parameters: {'created_date': datetime.datetime(2026, 9, 20, 14, 43, 29, 192202, tzinfo=datetime.timezone.utc), 'updated_date': datetime.datetime(2026, 9, 20, 14, 43, 29, 192209, tzinfo=datetime.timezone.utc), 'created_by_id': 'eaa21255-b5a6-434e-9c69-73fea7abbf22', 'created_by': '[email protected]', 'is_sample': False, 'id': 'c2181cf2-2470-4103-b552-0746c176940b', 'organization_id': '1111cf7b-0dc0-40f6-b20c-c35f640622f0', 'landlord_id': '00000000-0000-0000-0000-000000000000', 'user_id': None, 'first_name': 'John', 'last_name': 'Doe', 'email': '[email protected]', 'phone': '5551234567', 'emergency_contact_name': 'Jane Doe', 'emergency_contact_phone': '5559876543', 'status': 'prospect', 'notes': 'Test tenant', 'billing_email': None, 'currency': None, 'timesheet_period': None, 'logo_url': None, 'plan': None, 'user_limit': None, 'grace_period_start': None}]\n(Background on this error at: https://sqlalche.me/e/20/gkpj)"}

One bad request returns the constraint name, the entire tenant table schema, the exact INSERT, and a real account. That is the database boundary itself, handed to any logged-in caller.

Independent verdict, baseline: exploited = true — The HTTP response directly exposes a complete PostgreSQL database error including: table names ('tenant', 'user'), constraint name ('tenant_landlord_id_fkey'), the full SQL INSERT statement with all 24 column names, and the complete paramet…

And it isn't one endpoint. The leak lives in a handler every write funnels through, so the same exploit fires across entities and operations that have nothing to do with each other. 5 scenarios, 5 fired:

tenant · create ⚠event · create ⚠notification · create ⚠tenant · delete ⚠tenant · update ⚠

Anything that doesn't fire is dropped here — no false positive reaches the fix stage.

Opens →Now there's a real, confirmed bug — and a human fixing it, across every entry point, is exactly the weeks-per-finding this loop exists to collapse. So the fix has to be automatic too.

3 Fix it

The platform writes the fix — a runtime filter, no developer code change

The fix isn't a pull request waiting on a sprint. The platform generates a runtime filter, scoped to the exact request pattern that leaked, and hot-loads it at the gateway — one per scenario, surgical, no over-blocking. Here is the one it wrote, unedited:

FilterTenantCreateFkViolationLandlordIdAI-generated fix
import json
from typing import Union
from starlette.requests import Request
from starlette.responses import Response, JSONResponse
from src.security.base_filter import BaseFilter


class FilterTenantCreateFkViolationLandlordId(BaseFilter):
    def __init__(self, finding_type: str, config=None):
        super().__init__(finding_type, config)
    
    async def filter_request(self, request: Request) -> Union[Request, JSONResponse]:
        return request
    
    async def filter_response(self, request: Request, response: Response) -> Response:
        if request.method == "POST" and "/api/rensure/entity/tenant" in request.url.path:
            if response.status_code >= 400:
                try:
                    body = json.loads(response.body)
                    
                    if isinstance(body, dict) and "detail" in body:
                        detail = str(body["detail"])
                        
                        db_error_patterns = [
                            "psycopg2",
                            "foreignkeyviolation",
                            "sql:",
                            "[sql:",
                            "insert into",
                            "violates",
                            "constraint",
                            "sqlalchemy"
                        ]
                        
                        if any(pattern in detail.lower() for pattern in db_error_patterns):
                            return JSONResponse(
                                status_code=400,
                                content={"detail": "Unable to create tenant. Please verify your input and try again."}
                            )
                except (json.JSONDecodeError, AttributeError, TypeError):
                    pass
        
        return response
An AI-written fix is a claim, not a fix. It compiles. It looks right. Neither is evidence that it blocks the leak — or that it doesn't mangle a legitimate error along the way.

This is where every "AI fixes your code" demo stops. Generating the fix is the easy part. Whether you can trust it is decided entirely by what happens next.

Opens →An AI-written fix is untrusted until proven — and this one is a gateway filter: it blocks the leak at the edge, but the raw error is still thrown at the source. Fixing it where it originates is the next build; here, the filter has to earn its place first.

4 Confirm the fix

Re-run the same exploit — the verdict has to flip

The filter loads and the exact exploit from Step 2 runs again. Nothing about the attack changes — same request, same malformed landlord_id. Only the response is allowed to differ. It does:

Baseline · before
EXPLOITED
500 · {"detail":"(psycopg2.errors.ForeignKeyViolation) insert or update on t…
→
After the filter · same exploit
BLOCKED
400 · {"detail":"Unable to create tenant. Please verify your input and try a…
Independent verdict, post-fix: exploited = false — The response contains only a generic error message 'Unable to create tenant. Please verify your input and try again.' which does not expose any database internals. Despite the foreign key violation on landlord_id (referencing non-existent U…

5 of 5 exploited scenarios flipped to blocked — each with the captured HTTP to show for it. The CWE-200 loop closed as completed.

0
Initialize
DONE
1
Analyze
DONE
2
Attack scenarios
DONE
3
Exploit code
DONE
4
Baseline verdict
DONE
5
Security filter
DONE
6
Post-fix verdict
DONE

remediation 97761074 · attempt 1 · 5/5 scenarios blocked · started 12:35:32

The gate has to be able to say no

A confirm step that always rubber-stamps is theater. So here is the same step on a different finding in the same app — an auth endpoint that leaked account state. The platform generated a fix, re-ran the exact exploit, and captured the result. The response came back unchanged:

Auth finding · baseline
EXPLOITED
401 · “Account locked.
Try again later.”
→
After the fix · same exploit
STILL EXPLOITED
401 · “Account locked.
Try again later.”
Independent verdict, post-fix: exploited = true — the response still reveals account security state. The loop refused to certify the fix, and said which one and why.

That is the difference between a report and a gate. A report tells you a fix was written. A gate re-runs the attack and tells you whether it actually worked — and this one can come back red.

Opens →Blocking the exploit proves the hole is closed. It does not prove the fix left everything else working. The regression tests exist; wiring them into the loop so every fix is re-validated is next.

5 Check the blast radius

One fix, and the regression surface is the whole app

The leak lived in a shared handler — the one every create, update and delete funnels through (that's the starburst at the top of this page). We proved it on the operations above; the platform walks the code knowledge graph outward from that unit to count the rest of the surface a fix there has to answer for. The answer isn't one endpoint. It's almost all of them.

Coverage is counted per operation — a test only counts when its latest run passed on that exact entity and operation.

create83 / 94 covered
update50 / 61 covered
delete9 / 10 covered
165
Impacted operations across 97 entities
5 / 5
Exploits blocked after the fix
86%
Of that surface has a passing test

Green is what a passing test would catch if the fix broke it. Red is impacted but unverified. The gap is the risk — 23 of 165 flows the fix reaches have no passing test that would notice if it broke them. This is the reverse view: every unit that calls into the changed one.

Knowing what could break isn't the same as knowing it did.

Opens →A visible gap is still a gap. Rendering both directions — forward and reverse coverage on one worked example — and running the regression suite across that surface is what turns "could break" into "didn't."

Discovery is already automatic. Remediation has to be too.

Every scanner on the market can do the first rung. A growing number claim the third. What almost none of them do is the rungs in between and after — prove the bug fires, prove the AI's fix holds, and refuse to certify it when it doesn't. You saw the loop do exactly that: green for the tenant finding, red for the auth one, both on captured evidence.

That's the trust the whole thing is built to earn. No human wrote the fix — a human read the evidence and decided. And when a fix doesn't hold, the loop is the thing that says so, before production does.


Verified remediation — evidence summary

Finding
Information Exposure — raw database error leaked on entity create (CWE-200) at src/components/tenants/TenantForm.jsx:27
Sink
src/api/entity_router.py:195 — raise HTTPException(status_code=500, detail=str(e))
Exploit
POST /api/rensure/entity/tenant returned HTTP 500 with the table schema, the exact SQL and a real account.
Baseline verdict
exploited = true
Scenarios (5)
tenant create, event create, notification create, tenant delete, tenant update. Baseline exploited: 5.
Fix
FilterTenantCreateFkViolationLandlordId — a runtime filter that rewrites the leaky response at the gateway.
Post-fix
5 of 5 scenarios blocked; the CWE-200 loop closed as completed.
Blast radius
97 entities, 165 impacted operations (create 94, update 61, delete 10); 142 covered by a passing test, 23 with none (86% of the surface).
Auth contrast
on a separate finding the AI-written fix did not hold — the loop returned exploited = true after the fix and refused to certify it.

Questions this case study answers

How do you prove an AI security fix actually blocks the exploit?

Re-run the exact exploit that proved the bug, unchanged, against the fixed app, capture the full HTTP response, and let an independent verdict decide whether protected data came back. Here the same malformed request returned the table schema, the SQL and a real account before the fix, and was blocked after it: 5 of 5 exploited scenarios flipped to blocked.

What happens when an AI-generated security fix fails verification?

The loop refuses to certify it. On a separate authentication finding in the same app, the re-run still revealed account state after the fix, so it was marked still exploited instead of fixed, with the evidence showing why.

How do you know an AI fix didn't break other parts of the app?

List every operation that runs through the changed code and check which ones have a test that passed on its latest run. Here the fix reaches 165 create, update and delete operations across 97 entities; 142 have a passing test that would catch a break, and 23 don't. Those 23 are the risk.

Why does one security fix affect so much of the app?

Because the leak sat in a handler that every write goes through. A fix there has to hold for every create, update and delete in the app, not just the one screen where the bug was found.

This is the overview. Each rung it left open gets its own piece next.
Origin-level fixes over edge filters · regression tests wired into the loop · forward-and-reverse coverage on one worked example — one deep dive each, closing one open risk at a time.
Rohit Chaudhri · Book an intro call · bringforth.ai

Read the full article, with the complete evidence: https://bringforth.ai/verification-scope