diff --git a/indexer-js-queue-handler/package-lock.json b/indexer-js-queue-handler/package-lock.json index ba52ba674..b055f6e01 100644 --- a/indexer-js-queue-handler/package-lock.json +++ b/indexer-js-queue-handler/package-lock.json @@ -15,6 +15,7 @@ "near-api-js": "1.1.0", "node-fetch": "^3.3.0", "pg": "^8.11.1", + "pg-format": "^1.0.4", "pluralize": "^8.0.0", "verror": "^1.10.1", "vm2": "^3.9.13" @@ -6565,6 +6566,14 @@ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", "integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==" }, + "node_modules/pg-format": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pg-format/-/pg-format-1.0.4.tgz", + "integrity": "sha512-YyKEF78pEA6wwTAqOUaHIN/rWpfzzIuMh9KdAhc3rSLQ/7zkRFcCgYBAEGatDstLyZw4g0s9SNICmaTGnBVeyw==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/pg-int8": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", @@ -12899,6 +12908,11 @@ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", "integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==" }, + "pg-format": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pg-format/-/pg-format-1.0.4.tgz", + "integrity": "sha512-YyKEF78pEA6wwTAqOUaHIN/rWpfzzIuMh9KdAhc3rSLQ/7zkRFcCgYBAEGatDstLyZw4g0s9SNICmaTGnBVeyw==" + }, "pg-int8": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", diff --git a/indexer-js-queue-handler/package.json b/indexer-js-queue-handler/package.json index 78ef53ebf..0ff9c7b24 100644 --- a/indexer-js-queue-handler/package.json +++ b/indexer-js-queue-handler/package.json @@ -19,6 +19,7 @@ "near-api-js": "1.1.0", "node-fetch": "^3.3.0", "pg": "^8.11.1", + "pg-format": "^1.0.4", "pluralize": "^8.0.0", "verror": "^1.10.1", "vm2": "^3.9.13" diff --git a/indexer-js-queue-handler/provisioner.js b/indexer-js-queue-handler/provisioner.js index 29b30d10a..9bf0d15e5 100644 --- a/indexer-js-queue-handler/provisioner.js +++ b/indexer-js-queue-handler/provisioner.js @@ -1,6 +1,7 @@ import VError from "verror"; import pg from "pg"; import cryptoModule from "crypto"; +import pgFormatModule from "pg-format"; import HasuraClient from "./hasura-client.js"; @@ -20,11 +21,13 @@ export default class Provisioner { constructor( hasuraClient = new HasuraClient(), pgPool = pool, - crypto = cryptoModule + crypto = cryptoModule, + pgFormat = pgFormatModule ) { this.hasuraClient = hasuraClient; this.pgPool = pgPool; this.crypto = crypto; + this.pgFormat = pgFormat; } async query(query, params = []) { @@ -46,16 +49,16 @@ export default class Provisioner { } async createDatabase(name) { - await this.query(`CREATE DATABASE ${name}`); + await this.query(this.pgFormat('CREATE DATABASE %I', name)); } async createUser(name, password) { - await this.query(`CREATE USER ${name} WITH PASSWORD '${password}';`) + await this.query(this.pgFormat(`CREATE USER %I WITH PASSWORD '%I'`, name, password)) } async restrictDatabaseToUser(databaseName, userName) { - await this.query(`GRANT ALL PRIVILEGES ON DATABASE ${databaseName} TO ${userName};`); - await this.query(`REVOKE CONNECT ON DATABASE ${databaseName} FROM PUBLIC;`); + await this.query(this.pgFormat('GRANT ALL PRIVILEGES ON DATABASE %I TO %I', databaseName, userName)); + await this.query(this.pgFormat('REVOKE CONNECT ON DATABASE %I FROM PUBLIC', databaseName)); } async createUserDb(name, password) { diff --git a/indexer-js-queue-handler/provisioner.test.js b/indexer-js-queue-handler/provisioner.test.js index e7157fac1..1fb5906a5 100644 --- a/indexer-js-queue-handler/provisioner.test.js +++ b/indexer-js-queue-handler/provisioner.test.js @@ -62,9 +62,9 @@ describe('Provisioner', () => { expect(pgClient.query.mock.calls).toEqual([ ['CREATE DATABASE morgs_near', []], - ['CREATE USER morgs_near WITH PASSWORD \'password\';', []], - ['GRANT ALL PRIVILEGES ON DATABASE morgs_near TO morgs_near;', []], - ['REVOKE CONNECT ON DATABASE morgs_near FROM PUBLIC;', []], + ['CREATE USER morgs_near WITH PASSWORD \'password\'', []], + ['GRANT ALL PRIVILEGES ON DATABASE morgs_near TO morgs_near', []], + ['REVOKE CONNECT ON DATABASE morgs_near FROM PUBLIC', []], ]); expect(hasuraClient.addDatasource).toBeCalledWith(userName, password, databaseName); expect(hasuraClient.runSql).toBeCalledWith(databaseName, databaseSchema); @@ -84,6 +84,19 @@ describe('Provisioner', () => { ); }); + it('formats user input before executing the query', async () => { + const provisioner = new Provisioner(hasuraClient, pgPool, crypto); + + await provisioner.createUserDb('morgs_near UNION SELECT * FROM users --', 'pass; DROP TABLE users;--'); + + expect(pgClient.query.mock.calls).toEqual([ + ['CREATE DATABASE "morgs_near UNION SELECT * FROM users --"', []], + ["CREATE USER \"morgs_near UNION SELECT * FROM users --\" WITH PASSWORD '\"pass; DROP TABLE users;--\"'", []], + ["GRANT ALL PRIVILEGES ON DATABASE \"morgs_near UNION SELECT * FROM users --\" TO \"morgs_near UNION SELECT * FROM users --\"", []], + ["REVOKE CONNECT ON DATABASE \"morgs_near UNION SELECT * FROM users --\" FROM PUBLIC", []] + ]); + }); + it('throws an error when it fails to create a postgres db', async () => { pgClient.query = jest.fn().mockRejectedValue(error);