Tagged template literals were added to the JavaScript as part of the 2015 update to the ECMAScript standard.
While a fair bit has been written about them, I’m going to argue their significance is underappreciated and I’m hoping this post will help change that. In part, it’s significant because it solves a problem people had resigned themselves to living with: SQL injection.
Before ES 2015, combining query strings with variables was done via concatenation using the plus operator.
It’s common to retrieve things from the database with information (like an product id) supplied by users, so code like this was common and resulted in many security vulnerabilities.
let query = "select * from widgets where id = " + 1 + ";"
As of ES 2015, you can now write this differently, by creating multi-line strings using backticks, and use ${} for variable interpolation within them.
let query = `select * from widgets where id = ${1};`
This is a lot nicer (less “noise”) but still problematic security-wise. It’s pairing this new syntax with another language feature known as tagged templates that give us the tools to solve sql injections once and for all:
let id = 1
// define a function to use as a "tag"
sql = (strings, ...vars) => ({strings, vars})
[Function: sql]
// call our "tag" function with a template literal
sql`select * from widgets where id = ${id};`
{ strings: [ 'select * from widgets where id = ', ';' ], vars: [ 1 ] }
What you see above is still just a function call, but it no longer works the same. Instead of doing the variable interpolation first and then calling the sql function with the resulting string (select * from widgets where id = 1;), the sql function is passed an array of string segments and the variables that are supposed to be interpolated.
You can see how different this is from the standard evaluation process by adding brackets to make this a standard function invocation; it switches back to the standard function invocation… the string is once again interpolated before being passed to the sql function, entirely losing the distinction between the value (which we probably don’t trust) and the string (that we probably do). The result is an injected string and an empty array of variables.
sql(`select * from widgets where id = ${id};`)
{ strings: 'select * from widgets where id = 1;', vars: [] }
This loss of context (the distinction between the variables/values and the query itself) is the heart of matter when it comes to SQL injection (or injection attacks generally). The moment the strings and variables are combined you have a problem on your hands.
So why not just use parameterized queries or something similar? It’s generally held that good code expresses the programmers intent. I would argue that select * from widgets where id = ${id}; perfectly expresses the programmers intent; the programmer wants the id variable to be included in the query string.
When the clearest expression of a programmers intent is also a security problem what you have is a systemic issue which requires a systemic fix.
This is why despite years of condescending security training, developer shaming and “push left” pep-talks SQL injection stubbornly remains “the hack that will never go away”. Pointing out the problem is easy, but providing implementable solutions is hard (especially since most security people don’t write code). As others have pointed out:
First, we need to deal with the standing advice of “Don’t trust your input.” This advice doesn’t give the programmers any actionable solution: what to trust, and how to build trust?
Given how ineffective the security industry has been so far, it’s fascinating to see Mike Samuel from Google’s security team as the champion of the “Template Strings” proposal. Even more telling is the mantra from his GitHub profile: “make the easiest way to express an idea in code a secure way to express that idea”.
You can see the fruits of his labour by noticing library authors leveraging this to deliver a great developer experience while doing the right thing for security. Allan Plum, the driving force behind the Arangodb Javascript driver leveraging tagged template literals to let users query ArangoDB safely.
The aql (Arango Query Language) function lets you write what would in any other language be an intent revealing SQL injection, safely returns an object with a query and some accompanying bindvars.
aql`FOR thing IN collection FILTER thing.foo == ${foo} RETURN thing`
{ query: 'FOR thing IN collection FILTER thing.foo == @value0 RETURN thing',
bindVars: { value0: 'bar' } }
Mike Samuel himself has a number of node libraries that leverage Tagged Template Literals, among them one to safely handle shell commands.
sh`echo -- ${a} "${b}" 'c: ${c}'`
It’s important to point out that Tagged Template Literals don’t entirely solve SQL injections, since there are no guarantees that any particular tag function will do “the right thing” security-wise, but the arguments the tag function receives set library authors up for success.
Authors using them get to offer an intuitive developer experience rather than the clunkiness of prepared statements, even though the tag function may well be using them under the hood. The best experience is from safest thing; It’s a great example of creating a “pit of success” for people to fall into.
// Good security hinges on devs learning to write clunky
// stuff like this instead of the simple stuff above.
const ps = new sql.PreparedStatement(/* [pool] */)
ps.input('param', sql.Int)
ps.prepare('select * from widgets where id = @id;', err => {
// ... error checks
ps.execute({id: 1}, (err, result) => {
// ... error checks
ps.unprepare(err => {
// ... error checks
})
})
})
It’s an interesting thought that JavaScripts deficiencies seem to have become it’s strength. First Ryan Dahl filled out the missing IO pieces to create Node JS and now missing features like multi-line string support provide an opportunity for some of the worlds most brilliant minds to insert cutting edge security features along-side these much needed fixes.
I’m really happy to finally see language level fixes for things that are clearly language level problems takes JavaScript next. It’s the only way I can see to move the needle in the security space and make perennial problems like “the hack that will never go away” finally go away.
