Voorkom SQL injecties met tagged template literals in Typescript

Jeroen Techniek

Ik ben fan van getypeerde programmeertalen, omdat type informatie een belangrijk hulpmiddel voor je is als programmeur. In een project hebben wij het gebruikt om onze code beter te beschermen tegen SQL injecties.

Een niet erg bekende feature van Javascript zijn tagged template literals. In Typescript kun je dit gebruiken om compile time te controleren of dynamische queries netjes gesanitized worden en je helpen beschermen tegen SQL injecties.

XKCD: Exploits of a Mom

Tagged template literals in Javascript

De meeste Javascript developers kennen en gebruiken regelmatig template literals. Template literals zijn een handige manier om strings met dynamische onderdelen op te bouwen.

const naam = 'Wereld'
const begroeting = `Hallo ${naam}!`;
console.log(begroeting)

//"Hallo Wereld!"

 

Bij een tagged template literal kun je een tag gebruiken als prefix. Deze tag is een functie die wordt aangeroepen. De template literal krijgt als waarde het resultaat van de functie. Zie hieronder.

Een tag functie krijgt als eerst parameter een array met alle vaste teksten. Daarna een individuele parameter met de waarde van elke expressie in de literal.

const naam = "Wereld";

function upper([voor, na], naam) {
  return voor + naam.toUpperCase() + na;
}

const begroeting = upper`Hallo ${naam}!`;
console.log(begroeting);

//"Hallo WERELD!"

 

Bovenstaande upper functie kun je veralgemeniseren om alle gebruikte expressies naar hoofdletters om te zetten door gebruik te maken van een rest parameter.

const naam = "Nederland";
const wie = "lezers";

function upper(strings, ...values) {
  return strings.reduce((query, string, i) => {
    const valueExists = i < values.length;
    const text = query + string;

    return valueExists ? text + values[i].toUpperCase() : text;
  }, "");
}

const begroeting = upper`Hallo ${wie} in ${naam}!`;
console.log(begroeting);

//"Hallo LEZERS in NEDERLAND!"

 

Voorkomen van sql injecties in Typescript

Je kunt een tagged template literal gebruiken om sql queries samen te stellen:

import * as mysql from "mysql";

type SanitizedString = string;

function escape(value: string): SanitizedString {
  return mysql.escape(value) as SanitizedString;
}

function sql(
  strings: TemplateStringsArray,
  ...values: SanitizedString[]
): SanitizedString {
  return strings.reduce((query, string, i) => {
    const valueExists = i < values.length;
    const text = query + string;

    return valueExists ? text + values[i] : text;
  }, "") as SanitizedString;
}

const unsafe_value = "Robert'; DROP TABLE students; --";

const unsafe_query = sql`SELECT * FROM students WHERE name = '${unsafe_value}'`;
const safe_query = sql`SELECT * FROM students WHERE name = ${escape(unsafe_value)}`;

console.log(unsafe_query);
console.log(safe_query);

// "SELECT * FROM students WHERE name = 'Robert'; DROP TABLE students; --'"
// "SELECT * FROM students WHERE name = 'Robert\'; DROP TABLE students; --'"

 

In dit voorbeeld wordt de escape functie van de mysql library gebruikt om onveilige waardes veilig te maken. Andere libraries voor andere databases hebben vergelijkbare functies.

De bovenstaande code laat het gebruik van de sql tag functie zien, maar het geeft ons (nog) geen type safety. In Typescript introduceert het type keyword een type alias en geen nieuw type. Dit betekent dat je string en Sanitized String door elkaar kan gebruiken. Wat je graag zou willen is dat deze twee types verschillend zijn. Het type systeem van typescript is echter ‘structural’ en niet ‘nominal’: Structural en Nominal types.

Je kunt echter wel grotendeels het gewenste gedrag krijgen door gebruik te maken van een intersection type en brands.

import * as mysql from "mysql";

interface SanitizedStringBrand {
  readonly _type: unique symbol;
}

type SanitizedString = SanitizedStringBrand & string;

function escape(value: string): SanitizedString {
  return mysql.escape(value) as SanitizedString;
}

function sql(
  strings: TemplateStringsArray,
  ...values: SanitizedString[]
): SanitizedString {

  return strings.reduce((query, string, i) => {
    const valueExists = i < values.length;
    const text = query + string;

    return valueExists ? text + values[i] : text;
  }, "") as SanitizedString;
}

const unsafe_value = "Robert'; DROP TABLE students; --";

/* The line below should not work */
const unsafe_query = sql`SELECT * FROM students WHERE name = '${unsafe_value}'`;
const query = sql`SELECT * FROM students WHERE name = ${escape(unsafe_value)}`;

console.log(unsafe_query);
console.log(query);

/*
* The first line shows a SQL injection that we have prevented.
* The second line shows the escaped query
*/
// "SELECT * FROM students WHERE name = 'Robert'; DROP TABLE students; --'"
// "SELECT * FROM students WHERE name = 'Robert\'; DROP TABLE students; --'"

 

De essentie van de oplossing zit erin dat de values parameter van de sql functie type SanitizedString[] heeft.

Omdat we nu het intersection type gebruiken zijn de types string en SanitizedString niet onderling uitwisselbaar en krijg je een compiler error:

Unsafe Query Compile Error

Je kunt een SanitizedString wel gebruiken op een plek waar een string verwacht wordt. Door een executeQuery functie te maken die SanitizedString en de vervolgens de query functionaliteit van je database library aanroept kun je afdwingen dat alleen veilige strings aangeboden worden. Het is nu eenvoudig geworden om tijdens een code review mogelijke problemen te herkennen. Je moet daarvoor slechts twee dingen in de gaten houden:

  1. Gebruik alleen de executeQuery functie die SanitizedStrings verwacht om de database te queryen.
  2. Controleer overal waar een as SanitizedString staat of er ook daadwerkelijk een veilige string wordt teruggegeven en de escape functie van je database library correct wordt aangeroepen.

Dit soort extra ‘veiligheidsgaranties’ en hulp voor de programmeur zijn de reden waarom ik persoonlijk fan ben van getypeerde programmeertalen en graag de (vermeende) extra moeite steek in het toevoegen van type annotaties.

Javascript is tegenwoordig een rijke en een sterk evoluerende taal. In de code voorbeelden hierboven worden nog een aantal nieuwere Javascript of Typescript features gebruikt:

  • In de functie definitie function upper([voor, na], naam) wordt gebruikt gemaakt van (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) voor de eerste parameter.
  • In de SanitizedStringBrand declaratie: readonly _type: unique symbol; gebruiken we unique symbol.
  • Op een aantal plekken, bv function upper(strings, ...values), gebruiken we een rest parameter.

Een afspraak maken bij ons op kantoor of wil je even iemand spreken? Stuur ons een mail of bel met Jolanda.