CQL lets operators define functions in the database with CREATE FUNCTION and aggregates with CREATE AGGREGATE. When this feature is enabled and the injection point can reach DDL, an attacker defines a function whose body is arbitrary code and runs it inside the Cassandra server's JVM.
The enabling condition
User-defined functions run only when the node's configuration turns them on:
enable_user_defined_functions: true
By default this is off. Where it is on, a function body may be written in Java, and older versions also accepted a now-deprecated JavaScript (Nashorn) body. Both execute in the same JVM as the database process, so a function body is a code-execution primitive with the privileges of the Cassandra service account. Sandboxing options (enable_user_defined_functions_threads) constrain threading but do not make the body safe to expose.
Defining a malicious function
Where injection reaches a statement boundary that can carry DDL, a CREATE FUNCTION with a Java body plants the code:
CREATE FUNCTION app.exec(cmd text)
RETURNS NULL ON NULL INPUT
RETURNS text
LANGUAGE java
AS $$
try {
Process p = Runtime.getRuntime().exec(cmd);
java.util.Scanner s = new java.util.Scanner(p.getInputStream()).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
} catch (Exception e) { return e.toString(); }
$$;
Calling it then runs the command and, because the function returns text, can return the output into a SELECT:
SELECT app.exec('id') FROM system.local;
system.local is a single-row table present on every node, which makes it a convenient driver for a one-shot call. The deprecated JavaScript form follows the same shape with LANGUAGE javascript and a script body, useful against older clusters where it remains enabled.
Reaching the DDL from injection
CREATE FUNCTION is a standalone DDL statement, not a WHERE fragment, and a CQL BATCH accepts only INSERT, UPDATE, and DELETE, so it cannot carry DDL. Planting a UDF therefore needs a sink that submits a complete statement of its own: an administrative or query-builder interface that runs attacker-chosen CQL, or a driver configured to accept multiple statements per request. The two-step pattern is one request that defines the function and a second that calls it, which also sidesteps the single-statement-per-request limit of the native protocol.
Calling an existing malicious UDF
Where a usable function already exists, whether an operator-defined one or one planted earlier, injection only needs to call it. A function reference drops straight into a SELECT projection or a WHERE comparison:
' AND app.exec('whoami') = 'x' ALLOW FILTERING /*
Even when the result is not reflected, the side effect (the command running) still fires, and output can be recovered through the same boolean channel described in Blind inference by comparing the returned value character by character.
Aggregates for state
CREATE AGGREGATE combines a state function with a final function over a result set, which lets a body run once per row and accumulate state across a scan. This is useful where a single call is constrained but a scan over a table is reachable, turning each processed row into another execution of the injected body.