denodrivers / postgres

PostgreSQL driver for Deno

Home Page:https://denodrivers.github.io/postgres

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Easy to be insecure by accident.

qm3ster opened this issue · comments

It seems trivial that someone, especially unfamiliar with tagged template strings might write

client.queryArray(`SELECT ID, NAME FROM PEOPLE WHERE ID = ${arg}`)

where they were supposed to write

client.queryArray`SELECT ID, NAME FROM PEOPLE WHERE ID = ${arg}`

causing the arguments not to be escaped.

Perhaps it would be better to error or at least warn when typeof query_template_or_config === "string" but args.length === 0.
This means that all static statements can still be passed as

client.queryArray`SELECT ID, NAME FROM PEOPLE`

and dynamically-loaded argumentless statements as

client.queryArray([my_query]) 

Problem with this is that template strings without arguments are a valid statement you can execute (in fact one that I use widely across deno-postgres codebase)

This is a known problem when working with template strings, one that is extensively covered in the docs, however there is no practical solution that I know of to solve this since this is a feature that relies in behavior already defined by JavaScript, not something that can be controlled on the library side

I'm gonna close this, but I'm open to discussion if you find a way to deal with the issues I expressed above

I thought disallowing passing a non-array-wrapped string as the sole argument solves this.

In terms of runtime/js this can be a deprecation warning, and an error in the next major version, if:
typeof query_template_or_config === "string" yet args.length === 0
This works because template call syntax certaintly wraps single unparameterized strings into an array:

> function t(...args){console.log(...args)}
> t('a')
'a'
> t`a`
['a']

In terms of typescript types, it is possible to annotate a single overload as deprecated, (and remove it when it becomes a hard error)

queryArray<T extends Array<unknown>>(query: string, arg0: any, ...args: QueryArguments): Promise<QueryArrayResult<T>>;
/** @deprecated use tagged template string syntax or wrap in an array if you know what you're doing */
queryArray<T extends Array<unknown>>(query: string, ...args: QueryArguments): Promise<QueryArrayResult<T>>;

Just to reiterate, this change:

// preserves
client.queryArray(my_preserved_statement, arg0, arg1)
// preserves
client.queryArray`SELECT ID, NAME FROM PEOPLE`

// no, use the above
client.queryArray('SELECT ID, NAME FROM PEOPLE')

// this is how you get ants
client.queryArray(`SELECT ${x}, NAME FROM PEOPLE`)
// but if you *want* ants
client.queryArray([`SELECT ${x}, NAME FROM PEOPLE`])

This is pre 1.0. We don't deprecate things here, we just remove them 😆

The following is a request that people often get wrong cause they don't understand how the template string system works, nevertheless is a very common one

await client.queryArray(`SELECT * FROM ${TABLE_NAME}`);
// Also
await client.queryArray(`SELECT ${fields.join(", ")} FROM MY_TABLE`);

Writing this is with the template string syntax would break this query because qualifiers and keywords can't be provided as an argument in a prepared statement. The alternative here (passing them as an array) could prove problematic if they provide more items in the array, since it would assume we are dealing with a template string and attempt replacement of arguments (this wouldn't do much damage now, but it will once named parameters are implemented). There is no way to prevent this

// You mean if they did
await client.queryArray([`SELECT * FROM ${TABLE_NAME}`, arg0]);
// instead of
await client.queryArray([`SELECT * FROM ${TABLE_NAME}`], arg0);
// ?
// I see how that's a possible mistake to make.
// But after all, as soon as there's an argument, they can just write
await client.queryArray(`SELECT * FROM ${TABLE_NAME}`, arg0);
// already

I understand if this is too high friction, but my concern was the following scenario:

  1. there's a parametrized tagged template call
  2. it works
  3. there's a refactor where it's put in parentheses (randomly, or for example replaced with a ternary/map)
  4. it still works, no one suspects a thing
  5. hax

Where can I read about what is planned for named parameters?

await client.queryArray([`SELECT * FROM ${TABLE_NAME}`, arg0]);
// instead of
await client.queryArray([`SELECT * FROM ${TABLE_NAME}`], arg0);

No, the second one would do nothing. What I mean is that the first one would either throw because no placeholders are found or replace something that looks like a placeholder

it still works, no one suspects a thing

Pretty much

Where can I read about what is planned for named parameters?

#223

I haven't settled on a syntax yet, and I'm worried we might handicap ourselves here