Skip to content

Commit 3873c9f

Browse files
authored
Merge pull request #6046 from neo4j/unsafe-escape-6
Add unsafeEscapeOptions - Cherry Pick from 5 LTS
2 parents 53a661d + dcf4c76 commit 3873c9f

25 files changed

+391
-37
lines changed

.changeset/plenty-pants-fold.md

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
"@neo4j/graphql": patch
3+
---
4+
5+
Add `unsafeEscapeOptions` to `Neo4jGraphQL` features with the following flags:
6+
7+
- `disableRelationshipTypeEscaping` (default to `false`)
8+
- `disableNodeLabelEscaping` (defaults to `false`)
9+
10+
These flags remove the automatic escaping of node labels and relationship types in the generated Cypher.
11+
12+
For example, given the following schema:
13+
14+
```graphql
15+
type Actor {
16+
name: String!
17+
}
18+
19+
type Movie {
20+
title: String!
21+
actors: [Actor!]! @relationship(type: "ACTED IN", direction: OUT)
22+
}
23+
```
24+
25+
A GraphQL query going through the `actors` relationship:
26+
27+
```graphql
28+
query {
29+
movies {
30+
title
31+
actors {
32+
name
33+
}
34+
}
35+
}
36+
```
37+
38+
Will normally generate the following Cypher for the relationship:
39+
40+
```cypher
41+
MATCH (this:Movie)-[this0:`ACTED IN`]->(this1:Actor)
42+
```
43+
44+
The label `ACTED IN` is escaped by placing it inside backticks (`\``), as some characters in it are susceptible of code injection.
45+
46+
If the option `disableRelationshipTypeEscaping` is set in `Neo4jGraphQL`, this safety mechanism will be disabled:
47+
48+
```js
49+
new Neo4jGraphQL({
50+
typeDefs,
51+
features: {
52+
unsafeEscapeOptions: {
53+
disableRelationshipTypeEscaping: true,
54+
},
55+
},
56+
});
57+
```
58+
59+
Generating the following (incorrect) Cypher instead:
60+
61+
```cypher
62+
MATCH (this:Movie)-[this0:ACTED IN]->(this1:Actor)
63+
```
64+
65+
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.
66+
67+
> Warning: This is a safety mechanism to avoid Cypher injection. Changing these options may lead to code injection and an unsafe server.

packages/graphql/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
"@graphql-tools/resolvers-composition": "^7.0.0",
8080
"@graphql-tools/schema": "^10.0.0",
8181
"@graphql-tools/utils": "10.6.1",
82-
"@neo4j/cypher-builder": "2.3.0",
82+
"@neo4j/cypher-builder": "^2.4.0",
8383
"camelcase": "^6.3.0",
8484
"debug": "^4.3.4",
8585
"dot-prop": "^6.0.1",

packages/graphql/src/translate/authorization/compatibility/compile-predicate-return.ts

+15-5
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919

2020
import Cypher from "@neo4j/cypher-builder";
2121
import type { PredicateReturn } from "../../../types";
22+
import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context";
2223
import { compileCypher } from "../../../utils/compile-cypher";
24+
import { buildClause } from "../../utils/build-clause";
2325

2426
type CompiledPredicateReturn = {
2527
cypher: string;
@@ -33,10 +35,15 @@ type CompiledPredicateReturn = {
3335
* The subqueries contain variables required by the predicate, and if they are not compiled with the same
3436
* environment, the predicate will be referring to non-existent variables and will re-assign variable from the subqueries.
3537
*/
36-
export function compilePredicateReturn(
37-
predicateReturn: PredicateReturn,
38-
indexPrefix?: string
39-
): CompiledPredicateReturn {
38+
export function compilePredicateReturn({
39+
predicateReturn,
40+
indexPrefix,
41+
context,
42+
}: {
43+
predicateReturn: PredicateReturn;
44+
indexPrefix: string | undefined;
45+
context: Neo4jGraphQLTranslationContext;
46+
}): CompiledPredicateReturn {
4047
const result: CompiledPredicateReturn = { cypher: "", params: {} };
4148

4249
const { predicate, preComputedSubqueries } = predicateReturn;
@@ -52,7 +59,10 @@ export function compilePredicateReturn(
5259
}
5360
return predicateStr;
5461
});
55-
const { cypher, params } = predicateCypher.build({ prefix: `authorization_${indexPrefix || ""}` });
62+
const { cypher, params } = buildClause(predicateCypher, {
63+
context,
64+
prefix: `authorization_${indexPrefix || ""}`,
65+
});
5666
result.cypher = cypher;
5767
result.params = params;
5868
result.subqueries = subqueries;

packages/graphql/src/translate/authorization/compatibility/create-authorization-after-and-params.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@
1818
*/
1919

2020
import Cypher from "@neo4j/cypher-builder";
21-
import type { Node } from "../../../types";
2221
import type { AuthorizationOperation } from "../../../schema-model/annotation/AuthorizationAnnotation";
22+
import type { Node } from "../../../types";
23+
import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context";
2324
import {
24-
createAuthorizationAfterPredicateField,
2525
createAuthorizationAfterPredicate,
26+
createAuthorizationAfterPredicateField,
2627
} from "../create-authorization-after-predicate";
2728
import type { NodeMap } from "../types/node-map";
2829
import { compilePredicateReturn } from "./compile-predicate-return";
29-
import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context";
3030

3131
type AuthorizationAfterAndParams = {
3232
cypher: string;
@@ -68,7 +68,7 @@ export function createAuthorizationAfterAndParams({
6868
});
6969

7070
if (predicateReturn) {
71-
return compilePredicateReturn(predicateReturn, `${indexPrefix || "_"}after_`);
71+
return compilePredicateReturn({ predicateReturn, indexPrefix: `${indexPrefix || "_"}after_`, context });
7272
}
7373

7474
return undefined;
@@ -94,7 +94,7 @@ export function createAuthorizationAfterAndParamsField({
9494
});
9595

9696
if (predicateReturn) {
97-
return compilePredicateReturn(predicateReturn, `${indexPrefix || "_"}after_`);
97+
return compilePredicateReturn({ predicateReturn, indexPrefix: `${indexPrefix || "_"}after_`, context });
9898
}
9999

100100
return undefined;

packages/graphql/src/translate/authorization/compatibility/create-authorization-before-and-params.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@
1818
*/
1919

2020
import Cypher from "@neo4j/cypher-builder";
21-
import type { Node } from "../../../types";
2221
import type { AuthorizationOperation } from "../../../schema-model/annotation/AuthorizationAnnotation";
22+
import type { Node } from "../../../types";
23+
import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context";
2324
import {
24-
createAuthorizationBeforePredicateField,
2525
createAuthorizationBeforePredicate,
26+
createAuthorizationBeforePredicateField,
2627
} from "../create-authorization-before-predicate";
2728
import type { NodeMap } from "../types/node-map";
2829
import { compilePredicateReturn } from "./compile-predicate-return";
29-
import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context";
3030

3131
type AuthorizationBeforeAndParams = {
3232
cypher: string;
@@ -69,7 +69,7 @@ export function createAuthorizationBeforeAndParams({
6969
});
7070

7171
if (predicateReturn) {
72-
return compilePredicateReturn(predicateReturn, `${indexPrefix || "_"}before_`);
72+
return compilePredicateReturn({ predicateReturn, indexPrefix: `${indexPrefix || "_"}before_`, context });
7373
}
7474

7575
return undefined;
@@ -93,7 +93,7 @@ export function createAuthorizationBeforeAndParamsField({
9393
});
9494

9595
if (predicateReturn) {
96-
return compilePredicateReturn(predicateReturn, "_before_");
96+
return compilePredicateReturn({ predicateReturn, indexPrefix: "_before_", context });
9797
}
9898

9999
return undefined;

packages/graphql/src/translate/create-connect-and-params.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { createAuthorizationBeforeAndParams } from "./authorization/compatibilit
3636
import { createRelationshipValidationString } from "./create-relationship-validation-string";
3737
import { createSetRelationshipProperties } from "./create-set-relationship-properties";
3838
import { filterMetaVariable } from "./subscriptions/filter-meta-variable";
39+
import { buildClause } from "./utils/build-clause";
3940
import { createWhereNodePredicate } from "./where/create-where-predicate";
4041

4142
interface Res {
@@ -148,7 +149,7 @@ function createConnectAndParams({
148149
if (filters?.preComputedSubqueries?.length) {
149150
const columns = [new Cypher.NamedVariable(nodeName)];
150151
const caseWhereClause = caseWhere(new Cypher.Raw(predicate), columns);
151-
const { cypher } = caseWhereClause.build({ prefix: "aggregateWhereFilter" });
152+
const { cypher } = buildClause(caseWhereClause, { context, prefix: "aggregateWhereFilter" });
152153
subquery.push(cypher);
153154
} else {
154155
subquery.push(`\tWHERE ${predicate}`);
@@ -429,7 +430,7 @@ function getFilters({
429430
return [cypher, {}];
430431
});
431432

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

434435
if (result.cypher) {
435436
return {

packages/graphql/src/translate/create-connect-or-create-and-params.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { createAuthorizationAfterPredicate } from "./authorization/create-author
2929
import { createAuthorizationBeforePredicate } from "./authorization/create-authorization-before-predicate";
3030
import { parseWhereField } from "./queryAST/factory/parsers/parse-where-field";
3131
import { assertNonAmbiguousUpdate } from "./utils/assert-non-ambiguous-update";
32+
import { buildClause } from "./utils/build-clause";
3233
import { addCallbackAndSetParamCypher } from "./utils/callback-utils";
3334

3435
type CreateOrConnectInput = {
@@ -99,7 +100,7 @@ export function createConnectOrCreateAndParams({
99100
});
100101

101102
const query = Cypher.utils.concat(...wrappedQueries);
102-
return query.build({ prefix: `${varName}_` });
103+
return buildClause(query, { context, prefix: `${varName}_` });
103104
}
104105

105106
function createConnectOrCreatePartialStatement({

packages/graphql/src/translate/create-delete-and-params.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-tran
2323
import { caseWhere } from "../utils/case-where";
2424
import { checkAuthentication } from "./authorization/check-authentication";
2525
import { createAuthorizationBeforeAndParams } from "./authorization/compatibility/create-authorization-before-and-params";
26+
import { buildClause } from "./utils/build-clause";
2627
import createConnectionWhereAndParams from "./where/create-connection-where-and-params";
2728

2829
interface Res {
@@ -173,7 +174,10 @@ function createDeleteAndParams({
173174
new Cypher.NamedVariable(variableName),
174175
];
175176
const caseWhereClause = caseWhere(new Cypher.Raw(predicate), columns);
176-
const { cypher } = caseWhereClause.build({ prefix: "aggregateWhereFilter" });
177+
const { cypher } = buildClause(caseWhereClause, {
178+
context,
179+
prefix: "aggregateWhereFilter",
180+
});
177181
innerStrs.push(cypher);
178182
} else {
179183
innerStrs.push(`WHERE ${predicate}`);

packages/graphql/src/translate/create-disconnect-and-params.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { caseWhere } from "../utils/case-where";
2525
import { checkAuthentication } from "./authorization/check-authentication";
2626
import { createAuthorizationAfterAndParams } from "./authorization/compatibility/create-authorization-after-and-params";
2727
import { createAuthorizationBeforeAndParams } from "./authorization/compatibility/create-authorization-before-and-params";
28+
import { buildClause } from "./utils/build-clause";
2829
import createConnectionWhereAndParams from "./where/create-connection-where-and-params";
2930

3031
interface Res {
@@ -142,7 +143,7 @@ function createDisconnectAndParams({
142143
if (aggregationWhere) {
143144
const columns = [new Cypher.NamedVariable(relVarName), new Cypher.NamedVariable(variableName)];
144145
const caseWhereClause = caseWhere(new Cypher.Raw(predicate), columns);
145-
const { cypher } = caseWhereClause.build({ prefix: "aggregateWhereFilter" });
146+
const { cypher } = buildClause(caseWhereClause, { context, prefix: "aggregateWhereFilter" });
146147
subquery.push(cypher);
147148
} else {
148149
subquery.push(`WHERE ${predicate}`);

packages/graphql/src/translate/create-update-and-params.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import createDisconnectAndParams from "./create-disconnect-and-params";
4242
import { createRelationshipValidationString } from "./create-relationship-validation-string";
4343
import { createSetRelationshipProperties } from "./create-set-relationship-properties";
4444
import { assertNonAmbiguousUpdate } from "./utils/assert-non-ambiguous-update";
45+
import { buildClause } from "./utils/build-clause";
4546
import { addCallbackAndSetParam } from "./utils/callback-utils";
4647
import { getAuthorizationStatements } from "./utils/get-authorization-statements";
4748
import { getMutationFieldStatements } from "./utils/get-mutation-field-statements";
@@ -252,7 +253,10 @@ export default function createUpdateAndParams({
252253
new Cypher.NamedVariable(variableName),
253254
];
254255
const caseWhereClause = caseWhere(new Cypher.Raw(predicate), columns);
255-
const { cypher } = caseWhereClause.build({ prefix: "aggregateWhereFilter" });
256+
const { cypher } = buildClause(caseWhereClause, {
257+
context,
258+
prefix: "aggregateWhereFilter",
259+
});
256260
innerUpdate.push(cypher);
257261
} else {
258262
innerUpdate.push(`WHERE ${predicate}`);

packages/graphql/src/translate/translate-aggregate.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { DEBUG_TRANSLATE } from "../constants";
2323
import type { EntityAdapter } from "../schema-model/entity/EntityAdapter";
2424
import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context";
2525
import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory";
26+
import { buildClause } from "./utils/build-clause";
2627

2728
const debug = Debug(DEBUG_TRANSLATE);
2829

@@ -43,5 +44,5 @@ export function translateAggregate({
4344
const queryAST = queryASTFactory.createQueryAST({ resolveTree, entityAdapter, context });
4445
debug(queryAST.print());
4546
const clause = queryAST.buildNew(context);
46-
return clause.build();
47+
return buildClause(clause, { context });
4748
}

packages/graphql/src/translate/translate-create.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { QueryASTContext, QueryASTEnv } from "./queryAST/ast/QueryASTContext";
3030
import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory";
3131
import { isUnwindCreateSupported } from "./queryAST/factory/parsers/is-unwind-create-supported";
3232
import unwindCreate from "./unwind-create";
33+
import { buildClause } from "./utils/build-clause";
3334
import { getAuthorizationStatements } from "./utils/get-authorization-statements";
3435

3536
const debug = Debug(DEBUG_TRANSLATE);
@@ -152,7 +153,7 @@ export default async function translateCreate({
152153
];
153154
});
154155

155-
const createQueryCypher = createQuery.build({ prefix: "create_" });
156+
const createQueryCypher = buildClause(createQuery, { context, prefix: "create_" });
156157
const { cypher, params: resolvedCallbacks } = await callbackBucket.resolveCallbacksAndFilterCypher({
157158
cypher: createQueryCypher.cypher,
158159
});

packages/graphql/src/translate/translate-delete.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-tran
2525
import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory";
2626

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

2930
const debug = Debug(DEBUG_TRANSLATE);
3031

@@ -52,7 +53,7 @@ function translateUsingQueryAST({
5253
});
5354
debug(operationsTree.print());
5455
const clause = operationsTree.build(context, varName);
55-
return clause.build();
56+
return buildClause(clause, { context });
5657
}
5758
export function translateDelete({
5859
context,

packages/graphql/src/translate/translate-read.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { DEBUG_TRANSLATE } from "../constants";
2323
import type { EntityAdapter } from "../schema-model/entity/EntityAdapter";
2424
import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context";
2525
import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory";
26+
import { buildClause } from "./utils/build-clause";
2627

2728
const debug = Debug(DEBUG_TRANSLATE);
2829

@@ -45,5 +46,5 @@ export function translateRead({
4546
});
4647
debug(operationsTree.print());
4748
const clause = operationsTree.build(context, varName);
48-
return clause.build();
49+
return buildClause(clause, { context });
4950
}

packages/graphql/src/translate/translate-resolve-reference.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { DEBUG_TRANSLATE } from "../constants";
2323
import type { EntityAdapter } from "../schema-model/entity/EntityAdapter";
2424
import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context";
2525
import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory";
26+
import { buildClause } from "./utils/build-clause";
2627

2728
const debug = Debug(DEBUG_TRANSLATE);
2829

@@ -46,5 +47,5 @@ export function translateResolveReference({
4647
});
4748
debug(operationsTree.print());
4849
const clause = operationsTree.build(context, "this");
49-
return clause.build();
50+
return buildClause(clause, { context });
5051
}

packages/graphql/src/translate/translate-top-level-cypher.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-tran
2727
import { applyAuthentication } from "./authorization/utils/apply-authentication";
2828
import { QueryASTContext, QueryASTEnv } from "./queryAST/ast/QueryASTContext";
2929
import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory";
30+
import { buildClause } from "./utils/build-clause";
3031

3132
const debug = Debug(DEBUG_TRANSLATE);
3233

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

0 commit comments

Comments
 (0)