Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unsafeEscapeOptions - Cherry Pick from 5 LTS #6046

Merged
merged 1 commit into from
Mar 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .changeset/plenty-pants-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
"@neo4j/graphql": patch
---

Add `unsafeEscapeOptions` to `Neo4jGraphQL` features with the following flags:

- `disableRelationshipTypeEscaping` (default to `false`)
- `disableNodeLabelEscaping` (defaults to `false`)

These flags remove the automatic escaping of node labels and relationship types in the generated Cypher.

For example, given the following schema:

```graphql
type Actor {
name: String!
}

type Movie {
title: String!
actors: [Actor!]! @relationship(type: "ACTED IN", direction: OUT)
}
```

A GraphQL query going through the `actors` relationship:

```graphql
query {
movies {
title
actors {
name
}
}
}
```

Will normally generate the following Cypher for the relationship:

```cypher
MATCH (this:Movie)-[this0:`ACTED IN`]->(this1:Actor)
```

The label `ACTED IN` is escaped by placing it inside backticks (`\``), as some characters in it are susceptible of code injection.

If the option `disableRelationshipTypeEscaping` is set in `Neo4jGraphQL`, this safety mechanism will be disabled:

```js
new Neo4jGraphQL({
typeDefs,
features: {
unsafeEscapeOptions: {
disableRelationshipTypeEscaping: true,
},
},
});
```

Generating the following (incorrect) Cypher instead:

```cypher
MATCH (this:Movie)-[this0:ACTED IN]->(this1:Actor)
```

This can be useful in very custom scenarios where the Cypher needs to be tweaked or if the labels and types have already been escaped.

> Warning: This is a safety mechanism to avoid Cypher injection. Changing these options may lead to code injection and an unsafe server.
2 changes: 1 addition & 1 deletion packages/graphql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
"@graphql-tools/resolvers-composition": "^7.0.0",
"@graphql-tools/schema": "^10.0.0",
"@graphql-tools/utils": "10.6.1",
"@neo4j/cypher-builder": "2.3.0",
"@neo4j/cypher-builder": "^2.4.0",
"camelcase": "^6.3.0",
"debug": "^4.3.4",
"dot-prop": "^6.0.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@

import Cypher from "@neo4j/cypher-builder";
import type { PredicateReturn } from "../../../types";
import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context";
import { compileCypher } from "../../../utils/compile-cypher";
import { buildClause } from "../../utils/build-clause";

type CompiledPredicateReturn = {
cypher: string;
Expand All @@ -33,10 +35,15 @@ type CompiledPredicateReturn = {
* The subqueries contain variables required by the predicate, and if they are not compiled with the same
* environment, the predicate will be referring to non-existent variables and will re-assign variable from the subqueries.
*/
export function compilePredicateReturn(
predicateReturn: PredicateReturn,
indexPrefix?: string
): CompiledPredicateReturn {
export function compilePredicateReturn({
predicateReturn,
indexPrefix,
context,
}: {
predicateReturn: PredicateReturn;
indexPrefix: string | undefined;
context: Neo4jGraphQLTranslationContext;
}): CompiledPredicateReturn {
const result: CompiledPredicateReturn = { cypher: "", params: {} };

const { predicate, preComputedSubqueries } = predicateReturn;
Expand All @@ -52,7 +59,10 @@ export function compilePredicateReturn(
}
return predicateStr;
});
const { cypher, params } = predicateCypher.build({ prefix: `authorization_${indexPrefix || ""}` });
const { cypher, params } = buildClause(predicateCypher, {
context,
prefix: `authorization_${indexPrefix || ""}`,
});
result.cypher = cypher;
result.params = params;
result.subqueries = subqueries;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@
*/

import Cypher from "@neo4j/cypher-builder";
import type { Node } from "../../../types";
import type { AuthorizationOperation } from "../../../schema-model/annotation/AuthorizationAnnotation";
import type { Node } from "../../../types";
import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context";
import {
createAuthorizationAfterPredicateField,
createAuthorizationAfterPredicate,
createAuthorizationAfterPredicateField,
} from "../create-authorization-after-predicate";
import type { NodeMap } from "../types/node-map";
import { compilePredicateReturn } from "./compile-predicate-return";
import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context";

type AuthorizationAfterAndParams = {
cypher: string;
Expand Down Expand Up @@ -68,7 +68,7 @@ export function createAuthorizationAfterAndParams({
});

if (predicateReturn) {
return compilePredicateReturn(predicateReturn, `${indexPrefix || "_"}after_`);
return compilePredicateReturn({ predicateReturn, indexPrefix: `${indexPrefix || "_"}after_`, context });
}

return undefined;
Expand All @@ -94,7 +94,7 @@ export function createAuthorizationAfterAndParamsField({
});

if (predicateReturn) {
return compilePredicateReturn(predicateReturn, `${indexPrefix || "_"}after_`);
return compilePredicateReturn({ predicateReturn, indexPrefix: `${indexPrefix || "_"}after_`, context });
}

return undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@
*/

import Cypher from "@neo4j/cypher-builder";
import type { Node } from "../../../types";
import type { AuthorizationOperation } from "../../../schema-model/annotation/AuthorizationAnnotation";
import type { Node } from "../../../types";
import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context";
import {
createAuthorizationBeforePredicateField,
createAuthorizationBeforePredicate,
createAuthorizationBeforePredicateField,
} from "../create-authorization-before-predicate";
import type { NodeMap } from "../types/node-map";
import { compilePredicateReturn } from "./compile-predicate-return";
import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context";

type AuthorizationBeforeAndParams = {
cypher: string;
Expand Down Expand Up @@ -69,7 +69,7 @@ export function createAuthorizationBeforeAndParams({
});

if (predicateReturn) {
return compilePredicateReturn(predicateReturn, `${indexPrefix || "_"}before_`);
return compilePredicateReturn({ predicateReturn, indexPrefix: `${indexPrefix || "_"}before_`, context });
}

return undefined;
Expand All @@ -93,7 +93,7 @@ export function createAuthorizationBeforeAndParamsField({
});

if (predicateReturn) {
return compilePredicateReturn(predicateReturn, "_before_");
return compilePredicateReturn({ predicateReturn, indexPrefix: "_before_", context });
}

return undefined;
Expand Down
5 changes: 3 additions & 2 deletions packages/graphql/src/translate/create-connect-and-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { createAuthorizationBeforeAndParams } from "./authorization/compatibilit
import { createRelationshipValidationString } from "./create-relationship-validation-string";
import { createSetRelationshipProperties } from "./create-set-relationship-properties";
import { filterMetaVariable } from "./subscriptions/filter-meta-variable";
import { buildClause } from "./utils/build-clause";
import { createWhereNodePredicate } from "./where/create-where-predicate";

interface Res {
Expand Down Expand Up @@ -148,7 +149,7 @@ function createConnectAndParams({
if (filters?.preComputedSubqueries?.length) {
const columns = [new Cypher.NamedVariable(nodeName)];
const caseWhereClause = caseWhere(new Cypher.Raw(predicate), columns);
const { cypher } = caseWhereClause.build({ prefix: "aggregateWhereFilter" });
const { cypher } = buildClause(caseWhereClause, { context, prefix: "aggregateWhereFilter" });
subquery.push(cypher);
} else {
subquery.push(`\tWHERE ${predicate}`);
Expand Down Expand Up @@ -429,7 +430,7 @@ function getFilters({
return [cypher, {}];
});

const result = whereCypher.build({ prefix: `${nodeName}_` });
const result = buildClause(whereCypher, { context, prefix: `${nodeName}_` });

if (result.cypher) {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { createAuthorizationAfterPredicate } from "./authorization/create-author
import { createAuthorizationBeforePredicate } from "./authorization/create-authorization-before-predicate";
import { parseWhereField } from "./queryAST/factory/parsers/parse-where-field";
import { assertNonAmbiguousUpdate } from "./utils/assert-non-ambiguous-update";
import { buildClause } from "./utils/build-clause";
import { addCallbackAndSetParamCypher } from "./utils/callback-utils";

type CreateOrConnectInput = {
Expand Down Expand Up @@ -99,7 +100,7 @@ export function createConnectOrCreateAndParams({
});

const query = Cypher.utils.concat(...wrappedQueries);
return query.build({ prefix: `${varName}_` });
return buildClause(query, { context, prefix: `${varName}_` });
}

function createConnectOrCreatePartialStatement({
Expand Down
6 changes: 5 additions & 1 deletion packages/graphql/src/translate/create-delete-and-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-tran
import { caseWhere } from "../utils/case-where";
import { checkAuthentication } from "./authorization/check-authentication";
import { createAuthorizationBeforeAndParams } from "./authorization/compatibility/create-authorization-before-and-params";
import { buildClause } from "./utils/build-clause";
import createConnectionWhereAndParams from "./where/create-connection-where-and-params";

interface Res {
Expand Down Expand Up @@ -173,7 +174,10 @@ function createDeleteAndParams({
new Cypher.NamedVariable(variableName),
];
const caseWhereClause = caseWhere(new Cypher.Raw(predicate), columns);
const { cypher } = caseWhereClause.build({ prefix: "aggregateWhereFilter" });
const { cypher } = buildClause(caseWhereClause, {
context,
prefix: "aggregateWhereFilter",
});
innerStrs.push(cypher);
} else {
innerStrs.push(`WHERE ${predicate}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { caseWhere } from "../utils/case-where";
import { checkAuthentication } from "./authorization/check-authentication";
import { createAuthorizationAfterAndParams } from "./authorization/compatibility/create-authorization-after-and-params";
import { createAuthorizationBeforeAndParams } from "./authorization/compatibility/create-authorization-before-and-params";
import { buildClause } from "./utils/build-clause";
import createConnectionWhereAndParams from "./where/create-connection-where-and-params";

interface Res {
Expand Down Expand Up @@ -142,7 +143,7 @@ function createDisconnectAndParams({
if (aggregationWhere) {
const columns = [new Cypher.NamedVariable(relVarName), new Cypher.NamedVariable(variableName)];
const caseWhereClause = caseWhere(new Cypher.Raw(predicate), columns);
const { cypher } = caseWhereClause.build({ prefix: "aggregateWhereFilter" });
const { cypher } = buildClause(caseWhereClause, { context, prefix: "aggregateWhereFilter" });
subquery.push(cypher);
} else {
subquery.push(`WHERE ${predicate}`);
Expand Down
6 changes: 5 additions & 1 deletion packages/graphql/src/translate/create-update-and-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import createDisconnectAndParams from "./create-disconnect-and-params";
import { createRelationshipValidationString } from "./create-relationship-validation-string";
import { createSetRelationshipProperties } from "./create-set-relationship-properties";
import { assertNonAmbiguousUpdate } from "./utils/assert-non-ambiguous-update";
import { buildClause } from "./utils/build-clause";
import { addCallbackAndSetParam } from "./utils/callback-utils";
import { getAuthorizationStatements } from "./utils/get-authorization-statements";
import { getMutationFieldStatements } from "./utils/get-mutation-field-statements";
Expand Down Expand Up @@ -252,7 +253,10 @@ export default function createUpdateAndParams({
new Cypher.NamedVariable(variableName),
];
const caseWhereClause = caseWhere(new Cypher.Raw(predicate), columns);
const { cypher } = caseWhereClause.build({ prefix: "aggregateWhereFilter" });
const { cypher } = buildClause(caseWhereClause, {
context,
prefix: "aggregateWhereFilter",
});
innerUpdate.push(cypher);
} else {
innerUpdate.push(`WHERE ${predicate}`);
Expand Down
3 changes: 2 additions & 1 deletion packages/graphql/src/translate/translate-aggregate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { DEBUG_TRANSLATE } from "../constants";
import type { EntityAdapter } from "../schema-model/entity/EntityAdapter";
import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context";
import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory";
import { buildClause } from "./utils/build-clause";

const debug = Debug(DEBUG_TRANSLATE);

Expand All @@ -43,5 +44,5 @@ export function translateAggregate({
const queryAST = queryASTFactory.createQueryAST({ resolveTree, entityAdapter, context });
debug(queryAST.print());
const clause = queryAST.buildNew(context);
return clause.build();
return buildClause(clause, { context });
}
3 changes: 2 additions & 1 deletion packages/graphql/src/translate/translate-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { QueryASTContext, QueryASTEnv } from "./queryAST/ast/QueryASTContext";
import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory";
import { isUnwindCreateSupported } from "./queryAST/factory/parsers/is-unwind-create-supported";
import unwindCreate from "./unwind-create";
import { buildClause } from "./utils/build-clause";
import { getAuthorizationStatements } from "./utils/get-authorization-statements";

const debug = Debug(DEBUG_TRANSLATE);
Expand Down Expand Up @@ -152,7 +153,7 @@ export default async function translateCreate({
];
});

const createQueryCypher = createQuery.build({ prefix: "create_" });
const createQueryCypher = buildClause(createQuery, { context, prefix: "create_" });
const { cypher, params: resolvedCallbacks } = await callbackBucket.resolveCallbacksAndFilterCypher({
cypher: createQueryCypher.cypher,
});
Expand Down
3 changes: 2 additions & 1 deletion packages/graphql/src/translate/translate-delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-tran
import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory";

import type { ResolveTree } from "graphql-parse-resolve-info";
import { buildClause } from "./utils/build-clause";

const debug = Debug(DEBUG_TRANSLATE);

Expand Down Expand Up @@ -52,7 +53,7 @@ function translateUsingQueryAST({
});
debug(operationsTree.print());
const clause = operationsTree.build(context, varName);
return clause.build();
return buildClause(clause, { context });
}
export function translateDelete({
context,
Expand Down
3 changes: 2 additions & 1 deletion packages/graphql/src/translate/translate-read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { DEBUG_TRANSLATE } from "../constants";
import type { EntityAdapter } from "../schema-model/entity/EntityAdapter";
import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context";
import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory";
import { buildClause } from "./utils/build-clause";

const debug = Debug(DEBUG_TRANSLATE);

Expand All @@ -45,5 +46,5 @@ export function translateRead({
});
debug(operationsTree.print());
const clause = operationsTree.build(context, varName);
return clause.build();
return buildClause(clause, { context });
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { DEBUG_TRANSLATE } from "../constants";
import type { EntityAdapter } from "../schema-model/entity/EntityAdapter";
import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context";
import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory";
import { buildClause } from "./utils/build-clause";

const debug = Debug(DEBUG_TRANSLATE);

Expand All @@ -46,5 +47,5 @@ export function translateResolveReference({
});
debug(operationsTree.print());
const clause = operationsTree.build(context, "this");
return clause.build();
return buildClause(clause, { context });
}
3 changes: 2 additions & 1 deletion packages/graphql/src/translate/translate-top-level-cypher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-tran
import { applyAuthentication } from "./authorization/utils/apply-authentication";
import { QueryASTContext, QueryASTEnv } from "./queryAST/ast/QueryASTContext";
import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory";
import { buildClause } from "./utils/build-clause";

const debug = Debug(DEBUG_TRANSLATE);

Expand Down Expand Up @@ -84,5 +85,5 @@ export function translateTopLevelCypher({
const projectionStatements = queryASTResult.clauses.length
? Cypher.utils.concat(...queryASTResult.clauses)
: new Cypher.Return(new Cypher.Literal("Query cannot conclude with CALL"));
return projectionStatements.build();
return buildClause(projectionStatements, { context });
}
Loading
Loading