Skip to content

Cypher injection: order_by and where are interpolated raw into aggregation queries #40

Description

@ethan-long71

aggregation_query in the Neo4j client is careful about labels and property keys — it runs them through _sanitize_label / _sanitize_property_key — but then drops that discipline for the where and order_by fragments, splicing them into the query string verbatim.

aag/computing_engine/graph_query/graph_query.py:925:

            where_clause = f"WHERE {where}" if where else ""
            limit_clause = "LIMIT $limit" if limit is not None else ""
            if limit is not None:
                params["limit"] = limit
            
            cypher = f"""
            MATCH (n{label_pattern})
            {where_clause}
            RETURN n.`{prop}` AS {prop}, {agg_expr}
            ORDER BY {order_by or agg_alias} {order_direction}
            {limit_clause}
            """

Note prop is sanitized (_sanitize_property_key) and limit is parameterized, but where and order_by are trusted raw Cypher. These values originate from the natural-language query engine, i.e. they are derived from LLM output over user text — exactly the untrusted surface the sanitizers elsewhere exist to defend.

Failure scenario. The parameter extractor produces order_by = "total DESC RETURN n } WITH n MATCH (x) DETACH DELETE x //" (or a comparable where payload like 1=1 DETACH DELETE n //). Because it is concatenated directly, the resulting query breaks out of the intended read and performs a destructive write, or exfiltrates unrelated nodes. Even without malice, an order_by containing a bare property name with a space or backtick produces a syntax error that aborts the query.

Fix. Whitelist order_by against the known return aliases / sanitized property keys (reuse _sanitize_property_key), constrain order_direction to {ASC, DESC}, and build where from structured (key, op, value) triples with parameterized values instead of accepting a raw clause. The same raw-where/order_by pattern recurs in the sibling grouping branch a few dozen lines down and should get the same treatment.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions