Breaking Bots: Investigating SQL Injection via AWS Lex Chatbots
kangwijen

An Overview
I found this pattern while building a challenge for ITSEC CTF 2025. I wanted players to exploit a realistic cloud-native attack path, not just another web form. After reading MorattiSec's write-up on red team persistence through AWS Lex chatbots The crow flies at midnight, I started looking at Lex as a legitimate attack surface rather than an abstraction layer that removes classic web vulnerabilities.
The challenge is a meeting booking chatbot backed by a relational database. The bot collects a few inputs from the user such as: meeting topic, time, and email. The injection vector is the meeting topic slot, which is typed as AMAZON.FreeFormInput. Amazon Lex V2 does not execute SQL. The vulnerability exists entirely in how the Lambda fulfillment function processes that slot value.
The Challenge Setup
How Lex V2 Processes Input
Lex V2 classifies user messages into intents (the action the user wants to perform) and extracts slots (the specific data values needed to fulfill that intent). After slots are resolved, Lex invokes an AWS Lambda function and passes the session state, including all slot values, in a structured JSON event.
Most built-in slot types apply normalization. AMAZON.Date converts natural language like next Monday into an ISO date. AMAZON.Number converts five into 5. This normalization creates a natural expectation that Lex is cleaning input before it reaches your backend.
AMAZON.FreeFormInput breaks that expectation entirely. AWS documents it as follows:
AMAZON.FreeFormInputcan be used to capture free form input as-is from the end user. The resolved value is the entire input utterance.
This means whatever the user types is forwarded as-is to Lambda with no transformation, no filtering, and no character restriction.
Request Flow
Slot Type Comparison
Slot Type | Example User Input | Lex Output Behavior | Security Impact |
|---|---|---|---|
|
| Normalized to a standard date format (for example | Lower parsing ambiguity in backend logic |
|
| Normalized to numeric value (for example | Lower type confusion risk |
|
| Passed through as-is, full utterance unchanged | High risk if concatenated into SQL |
The Vulnerable Lambda Code
The Lambda fulfillment handler reads the topic slot value from the Lex event and concatenates it directly into a SQL query string.
slots = event["sessionState"]["intent"]["slots"]
topic = slots["Topic"]["value"]["interpretedValue"]
time = slots["Time"]["value"]["interpretedValue"]
email = slots["Email"]["value"]["interpretedValue"]
# VULNERABLE: user-controlled values are concatenated into SQL
query = f"INSERT INTO bookings (topic, time, email) VALUES ('{topic}', '{time}', '{email}')"
cursor.execute(query)When a normal user inputs Team sync, the query becomes:
INSERT INTO bookings (topic, time, email) VALUES ('Team sync', '10:00', 'user@example.com')When an attacker inputs ' OR '1'='1 as the topic, the query becomes:
INSERT INTO bookings (topic, time, email) VALUES ('' OR '1'='1', '10:00', 'user@example.com')Exploitation
The first signal during testing was the raw MySQL error being returned by the bot after I entered a topic starting with a single quote. The response exposed:
Error: (1064, "You have an error in your SQL syntax ...')This confirms two issues at once. User input was reaching SQL construction unmodified, and backend database errors were being leaked directly to the chat UI.
Before the SQLi verification step, I also tested a one-liner fuzz payload for multiple bug classes such as SQLi, XSS, command injection, and SSRF. Most fields were handled better than expected. Date, time, and email rejected malformed values. Recipient name and meeting topic were more permissive, which made them the strongest candidates for deeper SQLi testing.
From there, an error-based SQLi payload path confirmed the backend database behavior. In the challenge environment, this progressed to schema enumeration and targeted extraction because the vulnerable query path was reachable through the permissive slot and the SQL error was reflected in the bot response.
The Fix
Use the Correct Slot Type First
Use strict built-in or custom slot types whenever possible. In this challenge, time and email should use constrained slot types, while only truly open-ended fields should use AMAZON.FreeFormInput. Reducing free-form capture narrows the injection surface before the payload reaches Lambda.
Use Parameterized Queries for All Database Writes
The correct fix is to bind slot values as parameters rather than interpolating them into the query string. With parameterized queries, the database engine treats the value as data regardless of its content.
# SAFE: slot value is bound as a parameter, not concatenated
query = "INSERT INTO bookings (topic, time, email) VALUES (%s, %s, %s)"
cursor.execute(query, (topic, time, email))With this in place, inputting ' OR '1'='1 as topic produces the literal string ' OR '1'='1 stored in the database column, not an altered query structure.
Final Thoughts
The vulnerability in this challenge exists because AMAZON.FreeFormInput passes user text to Lambda as-is and the Lambda function constructs SQL through string concatenation. Neither Lex nor Lambda prevents this on its own.
If you want the full exploitation chain for the challenge, including payload progression and the exact flag recovery flow, read the official writeup here: ITSEC Asia Summit 2025 Cloud Challenge: rabbit-hole