Skip to content

Commit

Permalink
fix: "hstore injection" & properly handle NULL, empty string, backsla…
Browse files Browse the repository at this point in the history
…shes & quotes in hstore key/value pairs (#4720)

* Improve HStore object support

* Add hstore-injection test
  • Loading branch information
tobyhinloopen authored and pleerock committed Sep 13, 2019
1 parent 644c21b commit 3abe5b9
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 9 deletions.
27 changes: 18 additions & 9 deletions src/driver/postgres/PostgresDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,9 +424,18 @@ export class PostgresDriver implements Driver {
if (typeof value === "string") {
return value;
} else {
return Object.keys(value).map(key => {
return `"${key}"=>"${value[key]}"`;
}).join(", ");
// https://www.postgresql.org/docs/9.0/hstore.html
const quoteString = (value: unknown) => {
// If a string to be quoted is `null` or `undefined`, we return a literal unquoted NULL.
// This way, NULL values can be stored in the hstore object.
if (value === null || typeof value === "undefined") {
return "NULL";
}
// Convert non-null values to string since HStore only stores strings anyway.
// To include a double quote or a backslash in a key or value, escape it with a backslash.
return `"${`${value}`.replace(/(?=["\\])/g, "\\")}"`;
};
return Object.keys(value).map(key => quoteString(key) + "=>" + quoteString(value[key])).join(",");
}

} else if (columnMetadata.type === "simple-array") {
Expand Down Expand Up @@ -476,13 +485,13 @@ export class PostgresDriver implements Driver {

} else if (columnMetadata.type === "hstore") {
if (columnMetadata.hstoreType === "object") {
const regexp = /"(.*?)"=>"(.*?[^\\"])"/gi;
const matchValue = value.match(regexp);
const unescapeString = (str: string) => str.replace(/\\./g, (m) => m[1]);
const regexp = /"([^"\\]*(?:\\.[^"\\]*)*)"=>(?:(NULL)|"([^"\\]*(?:\\.[^"\\]*)*)")(?:,|$)/g;
const object: ObjectLiteral = {};
let match;
while (match = regexp.exec(matchValue)) {
object[match[1].replace(`\\"`, `"`)] = match[2].replace(`\\"`, `"`);
}
`${value}`.replace(regexp, (_, key, nullValue, stringValue) => {
object[unescapeString(key)] = nullValue ? null : unescapeString(stringValue);
return "";
});
return object;

} else {
Expand Down
12 changes: 12 additions & 0 deletions test/github-issues/4719/entity/Post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {Column, Entity, PrimaryGeneratedColumn, ObjectLiteral} from "../../../../src/index";

@Entity()
export class Post {

@PrimaryGeneratedColumn()
id: number;

@Column("hstore", { hstoreType: "object" })
hstoreObj: ObjectLiteral;

}
41 changes: 41 additions & 0 deletions test/github-issues/4719/issue-4719.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import "reflect-metadata";
import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../utils/test-utils";
import {Connection} from "../../../src/connection/Connection";
import {Post} from "./entity/Post";

describe("github issues > #4719 HStore with empty string values", () => {
let connections: Connection[];
before(async () => connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
enabledDrivers: ["postgres"]
}));
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));

it("should handle HStore with empty string keys or values", () => Promise.all(connections.map(async connection => {
const queryRunner = connection.createQueryRunner();
const postRepository = connection.getRepository(Post);

const post = new Post();
post.hstoreObj = {name: "Alice", surname: "A", age: 25, blank: "", "": "blank-key", "\"": "\"", foo: null};
const {id} = await postRepository.save(post);

const loadedPost = await postRepository.findOneOrFail(id);
loadedPost.hstoreObj.should.be.deep.equal(
{ name: "Alice", surname: "A", age: "25", blank: "", "": "blank-key", "\"": "\"", foo: null });
await queryRunner.release();
})));

it("should not allow 'hstore injection'", () => Promise.all(connections.map(async connection => {
const queryRunner = connection.createQueryRunner();
const postRepository = connection.getRepository(Post);

const post = new Post();
post.hstoreObj = { username: `", admin=>"1`, admin: "0" };
const {id} = await postRepository.save(post);

const loadedPost = await postRepository.findOneOrFail(id);
loadedPost.hstoreObj.should.be.deep.equal({ username: `", admin=>"1`, admin: "0" });
await queryRunner.release();
})));
});

0 comments on commit 3abe5b9

Please sign in to comment.