Preventing TypeQL injection with TypeDB's new 'given' clause
Learn how query injection works, and how to prevent it in TypeQL with 'given' parameters.

We recently released TypeDB 3.12, which shipped a slew of new features. One of these is both a huge performance optimization and the proper way to prevent TypeQL injection.
What is an injection attack?
Injection attacks are probably the best-known class of database exploit. XKCD has a classic strip on this topic.
It’s possible in every database query language. In SQL (and similarly, Cypher), it arises from something like the following (Python):
name = request.form["name"] # user-controlled input, e.g. "Robert"cursor.execute(f"INSERT INTO students (name) VALUES ('{name}')")
This works fine until a malicious user submits a value that closes the string and continues the statement:
name = "Robert'); DROP TABLE students; --"# The database receives and happily executes:# INSERT INTO students (name) VALUES ('Robert'); DROP TABLE students; --')
Until recently, TypeDB’s API only really supported submitting queries as strings. This means you had to concatenate your query data into the query string – and now you’re open to an injection attack!
(New to TypeDB? Data is modeled directly as entities, relations, and attributes – the query below inserts a person entity that owns a name attribute. A TypeQL query is a pipeline of stages – like match, insert, and delete – each streaming rows into the next. The data and query model docs give a proper introduction.)
name = request.form["name"] # user-controlled input, e.g. "Robert"tx.query(f'insert $p isa person, has name "{name}";')
The same trick works here – the input closes the string, terminates the stage, and appends its own pipeline stages:
name = 'Robert"; match $q isa person; delete $q; insert $p2 isa person, has name "gotcha'# The query TypeDB receives:# insert $p isa person, has name "Robert";# match $q isa person;# delete $q;# insert $p2 isa person, has name "gotcha";
Notice that this is really mostly an issue for string inputs to the query.
String escaping
The instinctive, but error prone, solution is to ‘protect’ your inputs by escaping them. In other words, the issue is typically when the query parser reads a quote in your string, and accepts it as the end of the string.
Adding backslashes (the typical escape sequence) can work, and is what we required our users to do before.
However, it’s very easy to get wrong – for example, TypeDB supports both single and double quoting, and you have to remember to escape both. There’s also room for extra trickiness – what if there’s a backslash character already there before the quote – will it get treated correctly? Or what if you’re using the HTTP API, which also might need to escape data for transmission?
When this level of complexity arises in something conceptually simple, it’s best to seek completely different solutions.
Parameterized queries
The industry standard is to provide the “parameters” (e.g. your dynamically created values) to the query alongside the query.
In SQL, this could look like this:
cursor.execute("INSERT INTO students (name) VALUES (?)", (name,))
(The placeholder syntax varies by driver – sqlite3 uses ?, psycopg uses %s – but the principle is the same.)
The key part is that the parameter values passed to the database are never parsed at all. This is dramatically simpler and eliminates the old injection pathway by construction.
TypeQL’s “given” parameters
TypeDB 3.12 ships with a new query clause called given. Using given creates a query that expects values to be passed in separately from the query string – it doesn’t get run until they arrive.
TypeQL’s given clause is strongly typed and requires you to indicate what kind of data will be passed in. TypeDB’s drivers provide APIs to pass your data into the query with the right types:
query = """given $name_str: string, $email_str: string;insert $p isa person, has name == $name_str, has email == $email_str;"""tx.query(query, given_rows=[ {"name_str": "Robert'); DROP TABLE students; --", "email_str": "bobby@tables.com"},]).resolve()
(Note the ==: $name_str holds a plain string value, not a name attribute, so the insert attaches a name attribute whose value equals the input.)
The values never touch the query string, so there is nothing to escape and nothing to parse – Bobby Tables is stored as a (strange) name, not executed. With that – we are now safe from query injection!
The beauty of TypeQL’s query model is that you can read any query as a data streaming pipeline, with query clauses accepting input streams and generating new output streams.
So, you can understand the given clause as a new pipeline stage that takes the input from the driver and streams it into the subsequent clause. Elegant!
With this understanding, you can also easily see why the driver API accepts a list of inputs to the query, not just a single input: the given clause converts the entire list of inputs into a stream of input rows to the query.
tx.query(query, given_rows=[ {"name_str": "Alice", "email_str": "alice@example.com"}, {"name_str": "Bob", "email_str": "bob@example.com"},]).resolve()
Conclusion
What should you take away from this?
1) Don’t forget that injection attacks are real!
2) Protect yourself using the new given clause
In fact, even if your application only ever generates application-controlled inputs and never allows users to input data that end up in a query, there are still substantial performance benefits to using given: TypeDB compiles the query template once and streams all your data rows through it in a single batch.
So, don’t dally too long – try the new given query construct! And if you’re interested, you can continue reading on our previous blog post on the new feature.
