diff --git a/src/db/core.ts b/src/db/core.ts index 30b2825..064710f 100644 --- a/src/db/core.ts +++ b/src/db/core.ts @@ -220,6 +220,21 @@ export function sql< return new SQLFragment(Array.prototype.slice.apply(literals), expressions); } +export function escapeIdentifier (identifier: string | number): string { + return `"${String(identifier).replace(/"/g, '""')}"`; +} + +export function unescapeIdentifier (escapedIdentifier: string): string { + if (escapedIdentifier.charAt(0) === '"') { + const innerEscapedIdentifier = escapedIdentifier.charAt(escapedIdentifier.length - 1) === '"' + ? escapedIdentifier.slice(1, -1) + : escapedIdentifier.slice(1); + return innerEscapedIdentifier.replace(/""/g, '"'); + } else { + return escapedIdentifier; + } +} + let preparedNameSeq = 0; export class SQLFragment { @@ -310,7 +325,10 @@ export class SQLFragment } else if (typeof expression === 'string') { // if it's a string, it should be a x.Table or x.Column type, so just needs quoting - result.text += expression.charAt(0) === '"' ? expression : `"${expression}"`; + // Re-escape identifier even if it "seems" to be already escaped. A malicious user + // could pass a string that starts with " but injects SQL code. Only trust escaped + // identifiers that we escape ourselves. + result.text += escapeIdentifier(unescapeIdentifier(expression)); } else if (expression instanceof DangerousRawString) { // Little Bobby Tables passes straight through ... @@ -351,19 +369,19 @@ export class SQLFragment } else if (expression === self) { // alias to the latest column, if applicable if (!currentColumn) throw new Error(`The 'self' column alias has no meaning here`); - result.text += `"${currentColumn}"`; + result.text += escapeIdentifier(currentColumn); } else if (expression instanceof ParentColumn) { // alias to the parent table (plus supplied column name) of a nested query, if applicable if (!parentTable) throw new Error(`The 'parent' table alias has no meaning here`); - result.text += `"${parentTable}"."${expression.value}"`; + result.text += `${escapeIdentifier(parentTable)}.${escapeIdentifier(expression.value)}`; } else if (expression instanceof ColumnNames) { // a ColumnNames-wrapped object -> quoted names in a repeatable order // OR a ColumnNames-wrapped array -> quoted array values const columnNames = Array.isArray(expression.value) ? expression.value : Object.keys(expression.value).sort(); - result.text += columnNames.map(k => `"${k}"`).join(', '); + result.text += columnNames.map(k => escapeIdentifier(k)).join(', '); } else if (expression instanceof ColumnValues) { // a ColumnValues-wrapped object OR array @@ -412,7 +430,7 @@ export class SQLFragment result.text += ')'; } else { - result.text += `"${columnName}" = `; + result.text += `${escapeIdentifier(columnName)} = `; this.compileExpression(columnValue instanceof ParentColumn ? columnValue : new Parameter(columnValue), result, parentTable, columnName); }