Skip to content

Commit 3abe5b9

Browse files
tobyhinloopenpleerock
authored andcommitted
fix: "hstore injection" & properly handle NULL, empty string, backslashes & quotes in hstore key/value pairs (#4720)
* Improve HStore object support * Add hstore-injection test
1 parent 644c21b commit 3abe5b9

File tree

3 files changed

+71
-9
lines changed

3 files changed

+71
-9
lines changed

src/driver/postgres/PostgresDriver.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -424,9 +424,18 @@ export class PostgresDriver implements Driver {
424424
if (typeof value === "string") {
425425
return value;
426426
} else {
427-
return Object.keys(value).map(key => {
428-
return `"${key}"=>"${value[key]}"`;
429-
}).join(", ");
427+
// https://www.postgresql.org/docs/9.0/hstore.html
428+
const quoteString = (value: unknown) => {
429+
// If a string to be quoted is `null` or `undefined`, we return a literal unquoted NULL.
430+
// This way, NULL values can be stored in the hstore object.
431+
if (value === null || typeof value === "undefined") {
432+
return "NULL";
433+
}
434+
// Convert non-null values to string since HStore only stores strings anyway.
435+
// To include a double quote or a backslash in a key or value, escape it with a backslash.
436+
return `"${`${value}`.replace(/(?=["\\])/g, "\\")}"`;
437+
};
438+
return Object.keys(value).map(key => quoteString(key) + "=>" + quoteString(value[key])).join(",");
430439
}
431440

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

477486
} else if (columnMetadata.type === "hstore") {
478487
if (columnMetadata.hstoreType === "object") {
479-
const regexp = /"(.*?)"=>"(.*?[^\\"])"/gi;
480-
const matchValue = value.match(regexp);
488+
const unescapeString = (str: string) => str.replace(/\\./g, (m) => m[1]);
489+
const regexp = /"([^"\\]*(?:\\.[^"\\]*)*)"=>(?:(NULL)|"([^"\\]*(?:\\.[^"\\]*)*)")(?:,|$)/g;
481490
const object: ObjectLiteral = {};
482-
let match;
483-
while (match = regexp.exec(matchValue)) {
484-
object[match[1].replace(`\\"`, `"`)] = match[2].replace(`\\"`, `"`);
485-
}
491+
`${value}`.replace(regexp, (_, key, nullValue, stringValue) => {
492+
object[unescapeString(key)] = nullValue ? null : unescapeString(stringValue);
493+
return "";
494+
});
486495
return object;
487496

488497
} else {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {Column, Entity, PrimaryGeneratedColumn, ObjectLiteral} from "../../../../src/index";
2+
3+
@Entity()
4+
export class Post {
5+
6+
@PrimaryGeneratedColumn()
7+
id: number;
8+
9+
@Column("hstore", { hstoreType: "object" })
10+
hstoreObj: ObjectLiteral;
11+
12+
}

test/github-issues/4719/issue-4719.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import "reflect-metadata";
2+
import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../utils/test-utils";
3+
import {Connection} from "../../../src/connection/Connection";
4+
import {Post} from "./entity/Post";
5+
6+
describe("github issues > #4719 HStore with empty string values", () => {
7+
let connections: Connection[];
8+
before(async () => connections = await createTestingConnections({
9+
entities: [__dirname + "/entity/*{.js,.ts}"],
10+
enabledDrivers: ["postgres"]
11+
}));
12+
beforeEach(() => reloadTestingDatabases(connections));
13+
after(() => closeTestingConnections(connections));
14+
15+
it("should handle HStore with empty string keys or values", () => Promise.all(connections.map(async connection => {
16+
const queryRunner = connection.createQueryRunner();
17+
const postRepository = connection.getRepository(Post);
18+
19+
const post = new Post();
20+
post.hstoreObj = {name: "Alice", surname: "A", age: 25, blank: "", "": "blank-key", "\"": "\"", foo: null};
21+
const {id} = await postRepository.save(post);
22+
23+
const loadedPost = await postRepository.findOneOrFail(id);
24+
loadedPost.hstoreObj.should.be.deep.equal(
25+
{ name: "Alice", surname: "A", age: "25", blank: "", "": "blank-key", "\"": "\"", foo: null });
26+
await queryRunner.release();
27+
})));
28+
29+
it("should not allow 'hstore injection'", () => Promise.all(connections.map(async connection => {
30+
const queryRunner = connection.createQueryRunner();
31+
const postRepository = connection.getRepository(Post);
32+
33+
const post = new Post();
34+
post.hstoreObj = { username: `", admin=>"1`, admin: "0" };
35+
const {id} = await postRepository.save(post);
36+
37+
const loadedPost = await postRepository.findOneOrFail(id);
38+
loadedPost.hstoreObj.should.be.deep.equal({ username: `", admin=>"1`, admin: "0" });
39+
await queryRunner.release();
40+
})));
41+
});

0 commit comments

Comments
 (0)