Skip to content

Commit 0c25aa1

Browse files
committed
More validation for query parameters
Driver expects query parameters to either be undefined/null or an object. This was previously not enforced and illegal parameters, like strings or arrays, were sent to the database. This resulted in a protocol violation and database closed the connection. Users were only able to see the actual error/stacktrace in the database logs. Driver received a `ServiceUnavailable` or `SessionExpired` error. This commit adds validation of query parameters. It also prohibits nodes, relationships and paths from being used as query parameters in the driver.
1 parent 54a0641 commit 0c25aa1

File tree

9 files changed

+171
-36
lines changed

9 files changed

+171
-36
lines changed

src/v1/internal/http/http-session.js

+4-8
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
import {WRITE} from '../../driver';
2121
import Session from '../../session';
22-
import {assertCypherStatement} from '../util';
22+
import {validateStatementAndParameters} from '../util';
2323
import {Neo4jError} from '../../error';
2424
import HttpRequestRunner from './http-request-runner';
2525
import {EMPTY_CONNECTION_HOLDER} from '../connection-holder';
@@ -37,15 +37,11 @@ export default class HttpSession extends Session {
3737
}
3838

3939
run(statement, parameters = {}) {
40-
if (typeof statement === 'object' && statement.text) {
41-
parameters = statement.parameters || {};
42-
statement = statement.text;
43-
}
44-
assertCypherStatement(statement);
40+
const {query, params} = validateStatementAndParameters(statement, parameters);
4541

4642
return this._requestRunner.beginTransaction().then(transactionId => {
4743
this._ongoingTransactionIds.push(transactionId);
48-
const queryPromise = this._requestRunner.runQuery(transactionId, statement, parameters);
44+
const queryPromise = this._requestRunner.runQuery(transactionId, query, params);
4945

5046
return queryPromise.then(streamObserver => {
5147
if (streamObserver.hasFailed()) {
@@ -55,7 +51,7 @@ export default class HttpSession extends Session {
5551
}
5652
}).then(streamObserver => {
5753
this._ongoingTransactionIds = this._ongoingTransactionIds.filter(id => id !== transactionId);
58-
return new Result(streamObserver, statement, parameters, this._serverInfoSupplier, EMPTY_CONNECTION_HOLDER);
54+
return new Result(streamObserver, query, params, this._serverInfoSupplier, EMPTY_CONNECTION_HOLDER);
5955
});
6056
});
6157
}

src/v1/internal/packstream-v1.js

+14-4
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ class Packer {
129129
}
130130
} else if (isIterable(x)) {
131131
return this.packableIterable(x, onError);
132+
} else if (x instanceof Node) {
133+
return this._nonPackableValue(`It is not allowed to pass nodes in query parameters, given: ${x}`, onError);
134+
} else if (x instanceof Relationship) {
135+
return this._nonPackableValue(`It is not allowed to pass relationships in query parameters, given: ${x}`, onError);
136+
} else if (x instanceof Path) {
137+
return this._nonPackableValue(`It is not allowed to pass paths in query parameters, given: ${x}`, onError);
132138
} else if (x instanceof Structure) {
133139
var packableFields = [];
134140
for (var i = 0; i < x.fields.length; i++) {
@@ -155,10 +161,7 @@ class Packer {
155161
}
156162
};
157163
} else {
158-
if (onError) {
159-
onError(newError("Cannot pack this value: " + x));
160-
}
161-
return () => undefined;
164+
return this._nonPackableValue(`Unable to pack the given value: ${x}`, onError);
162165
}
163166
}
164167

@@ -334,6 +337,13 @@ class Packer {
334337
disableByteArrays() {
335338
this._byteArraysSupported = false;
336339
}
340+
341+
_nonPackableValue(message, onError) {
342+
if (onError) {
343+
onError(newError(message, PROTOCOL_ERROR));
344+
}
345+
return () => undefined;
346+
}
337347
}
338348

339349
/**

src/v1/internal/util.js

+33-5
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,29 @@ function isEmptyObjectOrNull(obj) {
3939
}
4040

4141
function isObject(obj) {
42-
const type = typeof obj;
43-
return type === 'function' || type === 'object' && Boolean(obj);
42+
return typeof obj === 'object' && !Array.isArray(obj) && Boolean(obj);
43+
}
44+
45+
/**
46+
* Check and normalize given statement and parameters.
47+
* @param {string|{text: string, parameters: object}} statement the statement to check.
48+
* @param {object} parameters
49+
* @return {{query: string, params: object}} the normalized query with parameters.
50+
* @throws TypeError when either given query or parameters are invalid.
51+
*/
52+
function validateStatementAndParameters(statement, parameters) {
53+
let query = statement;
54+
let params = parameters || {};
55+
56+
if (typeof statement === 'object' && statement.text) {
57+
query = statement.text;
58+
params = statement.parameters || {};
59+
}
60+
61+
assertCypherStatement(query);
62+
assertQueryParameters(params);
63+
64+
return {query, params};
4465
}
4566

4667
function assertString(obj, objName) {
@@ -52,10 +73,17 @@ function assertString(obj, objName) {
5273

5374
function assertCypherStatement(obj) {
5475
assertString(obj, 'Cypher statement');
55-
if (obj.trim().length == 0) {
76+
if (obj.trim().length === 0) {
5677
throw new TypeError('Cypher statement is expected to be a non-empty string.');
5778
}
58-
return obj;
79+
}
80+
81+
function assertQueryParameters(obj) {
82+
if (!isObject(obj) && Boolean(obj)) {
83+
// objects created with `Object.create(null)` do not have a constructor property
84+
const constructor = obj.constructor ? ' ' + obj.constructor.name : '';
85+
throw new TypeError(`Query parameters are expected to either be undefined/null or an object, given:${constructor} ${obj}`);
86+
}
5987
}
6088

6189
function isString(str) {
@@ -66,7 +94,7 @@ export {
6694
isEmptyObjectOrNull,
6795
isString,
6896
assertString,
69-
assertCypherStatement,
97+
validateStatementAndParameters,
7098
ENCRYPTION_ON,
7199
ENCRYPTION_OFF
72100
}

src/v1/session.js

+4-8
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import StreamObserver from './internal/stream-observer';
2020
import Result from './result';
2121
import Transaction from './transaction';
2222
import {newError} from './error';
23-
import {assertCypherStatement} from './internal/util';
23+
import {validateStatementAndParameters} from './internal/util';
2424
import ConnectionHolder from './internal/connection-holder';
2525
import Driver, {READ, WRITE} from './driver';
2626
import TransactionExecutor from './internal/transaction-executor';
@@ -60,14 +60,10 @@ class Session {
6060
* @return {Result} - New Result
6161
*/
6262
run(statement, parameters = {}) {
63-
if (typeof statement === 'object' && statement.text) {
64-
parameters = statement.parameters || {};
65-
statement = statement.text;
66-
}
67-
assertCypherStatement(statement);
63+
const {query, params} = validateStatementAndParameters(statement, parameters);
6864

69-
return this._run(statement, parameters, (connection, streamObserver) =>
70-
connection.run(statement, parameters, streamObserver)
65+
return this._run(query, params, (connection, streamObserver) =>
66+
connection.run(query, params, streamObserver)
7167
);
7268
}
7369

src/v1/transaction.js

+3-7
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
*/
1919
import StreamObserver from './internal/stream-observer';
2020
import Result from './result';
21-
import {assertCypherStatement} from './internal/util';
21+
import {validateStatementAndParameters} from './internal/util';
2222
import {EMPTY_CONNECTION_HOLDER} from './internal/connection-holder';
2323
import Bookmark from './internal/bookmark';
2424

@@ -60,13 +60,9 @@ class Transaction {
6060
* @return {Result} New Result
6161
*/
6262
run(statement, parameters) {
63-
if(typeof statement === 'object' && statement.text) {
64-
parameters = statement.parameters || {};
65-
statement = statement.text;
66-
}
67-
assertCypherStatement(statement);
63+
const {query, params} = validateStatementAndParameters(statement, parameters);
6864

69-
return this._state.run(this._connectionHolder, new _TransactionStreamObserver(this), statement, parameters);
65+
return this._state.run(this._connectionHolder, new _TransactionStreamObserver(this), query, params);
7066
}
7167

7268
/**
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Copyright (c) 2002-2018 "Neo Technology,"
3+
* Network Engine for Objects in Lund AB [http://neotechnology.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
import sharedNeo4j from '../../internal/shared-neo4j';
20+
import urlUtil from '../../../src/v1/internal/url-util';
21+
import testUtil from '../test-utils';
22+
import HttpSession from '../../../src/v1/internal/http/http-session';
23+
import HttpSessionTracker from '../../../src/v1/internal/http/http-session-tracker';
24+
25+
describe('http session', () => {
26+
27+
it('should fail for invalid query parameters', done => {
28+
if (testUtil.isServer()) {
29+
done();
30+
return;
31+
}
32+
33+
const session = new HttpSession(urlUtil.parseDatabaseUrl('http://localhost:7474'), sharedNeo4j.authToken, {}, new HttpSessionTracker());
34+
35+
expect(() => session.run('RETURN $value', [1, 2, 3])).toThrowError(TypeError);
36+
expect(() => session.run('RETURN $value', '123')).toThrowError(TypeError);
37+
expect(() => session.run('RETURN $value', () => [123])).toThrowError(TypeError);
38+
39+
session.close(() => done());
40+
});
41+
42+
});

test/internal/util.test.js

+31-4
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,12 @@ describe('util', () => {
2424
it('should check empty objects', () => {
2525
expect(util.isEmptyObjectOrNull(null)).toBeTruthy();
2626
expect(util.isEmptyObjectOrNull({})).toBeTruthy();
27-
expect(util.isEmptyObjectOrNull([])).toBeTruthy();
27+
28+
expect(util.isEmptyObjectOrNull([])).toBeFalsy();
2829

2930
const func = () => {
3031
return 42;
3132
};
32-
expect(util.isEmptyObjectOrNull(func)).toBeTruthy();
33-
func.foo = 'bar';
3433
expect(util.isEmptyObjectOrNull(func)).toBeFalsy();
3534

3635
expect(util.isEmptyObjectOrNull()).toBeFalsy();
@@ -74,6 +73,26 @@ describe('util', () => {
7473
verifyInvalidCypherStatement(console.log);
7574
});
7675

76+
it('should check valid query parameters', () => {
77+
verifyValidQueryParameters(null);
78+
verifyValidQueryParameters(undefined);
79+
verifyValidQueryParameters({});
80+
verifyValidQueryParameters({a: 1, b: 2, c: 3});
81+
verifyValidQueryParameters({foo: 'bar', baz: 'qux'});
82+
});
83+
84+
it('should check invalid query parameters', () => {
85+
verifyInvalidQueryParameters('parameter');
86+
verifyInvalidQueryParameters(123);
87+
verifyInvalidQueryParameters([]);
88+
verifyInvalidQueryParameters([1, 2, 3]);
89+
verifyInvalidQueryParameters([null]);
90+
verifyInvalidQueryParameters(['1', '2', '3']);
91+
verifyInvalidQueryParameters(() => [1, 2, 3]);
92+
verifyInvalidQueryParameters(() => '');
93+
verifyInvalidQueryParameters(() => null);
94+
});
95+
7796
function verifyValidString(str) {
7897
expect(util.assertString(str, 'Test string')).toBe(str);
7998
}
@@ -83,7 +102,15 @@ describe('util', () => {
83102
}
84103

85104
function verifyInvalidCypherStatement(str) {
86-
expect(() => util.assertCypherStatement(str)).toThrowError(TypeError);
105+
expect(() => util.validateStatementAndParameters(str, {})).toThrowError(TypeError);
106+
}
107+
108+
function verifyValidQueryParameters(obj) {
109+
expect(() => util.validateStatementAndParameters('RETURN 1', obj)).not.toThrow();
110+
}
111+
112+
function verifyInvalidQueryParameters(obj) {
113+
expect(() => util.validateStatementAndParameters('RETURN 1', obj)).toThrowError(TypeError);
87114
}
88115

89116
});

test/v1/session.test.js

+30
Original file line numberDiff line numberDiff line change
@@ -1072,6 +1072,26 @@ describe('session', () => {
10721072
});
10731073
});
10741074

1075+
it('should fail for invalid query parameters', () => {
1076+
expect(() => session.run('RETURN $value', '42')).toThrowError(TypeError);
1077+
expect(() => session.run('RETURN $value', 42)).toThrowError(TypeError);
1078+
expect(() => session.run('RETURN $value', () => 42)).toThrowError(TypeError);
1079+
});
1080+
1081+
it('should fail to pass node as a query parameter', done => {
1082+
testUnsupportedQueryParameter(new neo4j.types.Node(neo4j.int(1), ['Person'], {name: 'Bob'}), done);
1083+
});
1084+
1085+
it('should fail to pass relationship as a query parameter', done => {
1086+
testUnsupportedQueryParameter(new neo4j.types.Relationship(neo4j.int(1), neo4j.int(2), neo4j.int(3), 'KNOWS', {since: 42}), done);
1087+
});
1088+
1089+
it('should fail to pass path as a query parameter', done => {
1090+
const node1 = new neo4j.types.Node(neo4j.int(1), ['Person'], {name: 'Alice'});
1091+
const node2 = new neo4j.types.Node(neo4j.int(2), ['Person'], {name: 'Bob'});
1092+
testUnsupportedQueryParameter(new neo4j.types.Path(node1, node2, []), done);
1093+
});
1094+
10751095
function serverIs31OrLater(done) {
10761096
if (serverVersion.compareTo(VERSION_3_1_0) < 0) {
10771097
done();
@@ -1176,4 +1196,14 @@ describe('session', () => {
11761196
});
11771197
}
11781198

1199+
function testUnsupportedQueryParameter(value, done) {
1200+
session.run('RETURN $value', {value: value}).then(() => {
1201+
done.fail(`Should not be possible to send ${value.constructor.name} ${value} as a query parameter`);
1202+
}).catch(error => {
1203+
expect(error.name).toEqual('Neo4jError');
1204+
expect(error.code).toEqual(neo4j.error.PROTOCOL_ERROR);
1205+
done();
1206+
});
1207+
}
1208+
11791209
});

test/v1/transaction.test.js

+10
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,16 @@ describe('transaction', () => {
509509
testConnectionTimeout(true, done);
510510
});
511511

512+
it('should fail for invalid query parameters', done => {
513+
const tx = session.beginTransaction();
514+
515+
expect(() => tx.run('RETURN $value', 'Hello')).toThrowError(TypeError);
516+
expect(() => tx.run('RETURN $value', 12345)).toThrowError(TypeError);
517+
expect(() => tx.run('RETURN $value', () => 'Hello')).toThrowError(TypeError);
518+
519+
tx.rollback().then(() => done());
520+
});
521+
512522
function expectSyntaxError(error) {
513523
expect(error.code).toBe('Neo.ClientError.Statement.SyntaxError');
514524
}

0 commit comments

Comments
 (0)