Most active commenters
  • Tenoke(9)
  • sanderjd(4)
  • ubercore(3)
  • thunky(3)

←back to thread

620 points tambourine_man | 42 comments | | HN request time: 2.998s | source | bottom
Show context
serbuvlad ◴[] No.43750075[source]
All things considered, this is pretty cool. Basically, this replaces

    db.execute("QUERY WHERE name = ?", (name,))
with

    db.execute(t"QUERY WHERE name = {name}")
Does the benefit from this syntactic sugar outweigh the added complexity of a new language feature? I think it does in this case for two reasons:

1. Allowing library developers to do whatever they want with {} expansions is a good thing, and will probably spawn some good uses.

2. Generalizing template syntax across a language, so that all libraries solve this problem in the same way, is probably a good thing.

replies(12): >>43750226 #>>43750250 #>>43750260 #>>43750279 #>>43750513 #>>43750750 #>>43752117 #>>43752173 #>>43752293 #>>43754738 #>>43756560 #>>43763190 #
1. Tenoke ◴[] No.43750250[source]
I don't see what it adds over f-string in that example?
replies(6): >>43750258 #>>43750261 #>>43750262 #>>43750265 #>>43750295 #>>43750581 #
2. evertedsphere ◴[] No.43750258[source]
safety against sql injection
3. ds_ ◴[] No.43750261[source]
The execute function can recognize it as a t-string and prevent SQL injection if the name is coming from user input. f-strings immediately evaluate to a string, whereas t-strings evaluate to a template object which requires further processing to turn it into a string.
replies(1): >>43750286 #
4. burky ◴[] No.43750262[source]
f-strings won’t sanitize the value, so it’s not safe. The article talks about this.
replies(1): >>43750427 #
5. teruakohatu ◴[] No.43750265[source]
If I pass an f-string to a method, it just sees a string. If I pass a t-string the method can decide how to process the t-string.
6. Tenoke ◴[] No.43750286[source]
Then the useful part is the extra execute function you have to write (it's not just a substitute like in the comment) and an extra function can confirm the safety of a value going into a f-string just as well.

I get the general case, but even then it seems like an implicit anti-pattern over doing db.execute(f"QUERY WHERE name = {safe(name)}")

replies(5): >>43750324 #>>43750380 #>>43750409 #>>43754093 #>>43756889 #
7. sureglymop ◴[] No.43750295[source]
Wouldn't this precisely lead to sql injection vulnerabilities with f-strings here?
8. ubercore ◴[] No.43750324{3}[source]
Problem with that example is where do you get `safe`? Passing a template into `db.execute` lets the `db` instance handle safety specifically for the backend it's connected to. Otherwise, you'd need to create a `safe` function with a db connection to properly sanitize a string.

And further, if `safe` just returns a string, you still lose out on the ability for `db.execute` to pass the parameter a different way -- you've lost the information that a variable is being interpolated into the string.

replies(1): >>43750412 #
9. NewEntryHN ◴[] No.43750380{3}[source]
Some SQL engines support accepting parameters separately so that values get bound to the query once the abstract syntax tree is already built, which is way safer than string escapes shenanigans.
replies(1): >>43751841 #
10. Mawr ◴[] No.43750409{3}[source]
But you have to remember to call the right safe() function every time:

    db.execute(f"QUERY WHERE name = {name}")

    db.execute(f"QUERY WHERE name = {safe_html(name)}")
Oops, you're screwed and there is nothing that can detect that. No such issue with a t-string, it cannot be misused.
11. Tenoke ◴[] No.43750412{4}[source]
db.safe same as the new db.execute with safety checks in it you create for the t-string but yes I can see some benefits (though I'm still not a fan for my own codebases so far) with using the values further or more complex cases than this.
replies(1): >>43750482 #
12. Tenoke ◴[] No.43750427[source]
The article talked about it but the example here just assumes they'll be there.
replies(2): >>43751261 #>>43751285 #
13. ubercore ◴[] No.43750482{5}[source]
Yeah but it would have to be something like `db.safe("SELECT * FROM table WHERE id = {}", row_id)` instead of `db.execute(t"SELECT * FROM table WHERE id = {row_id}")`.

I'd prefer the second, myself.

replies(2): >>43750548 #>>43753222 #
14. Tenoke ◴[] No.43750548{6}[source]
No, just `db.execute(f"QUERY WHERE name = {db.safe(name)}")`

And you add the safety inside db.safe explicitly instead of implicitly in db.execute.

If you want to be fancy you can also assign name to db.foos inside db.safe to use it later (even in execute).

replies(4): >>43750786 #>>43751243 #>>43751257 #>>43759309 #
15. sim7c00 ◴[] No.43750581[source]
it makes it so people too lazy to make good types and class will be getting closer to sane code without doing sane code...

imagine writing a SqL where u put user input into query string directly.

now remember its 2025, lie down try not to cry.

16. ZiiS ◴[] No.43750786{7}[source]
But if someone omits the `safe` it may still work but allow injection.
replies(1): >>43751449 #
17. sanderjd ◴[] No.43751243{7}[source]
This is just extra boilerplate though, for what purpose?.

I think one thing you might be missing is that in the t-string version, `db.execute` is not taking a string; a t-string resolves to an object of a particular type. So it is doing your `db.safe` operation, but automatically.

18. panzi ◴[] No.43751257{7}[source]
Of course you can write code like that. This is about making it easier not to accidentally cause code injection by forgetting the call of safe(). JavaScript had the same feature and some SQL libraries allow only the passing of template strings, not normal strings, so you can't generate a string with code injection. If you have to dynamically generate queries they allow that a parameter is another template string and then those are merged correctly. It's about reducing the likelihood of making mistakes with fewer key strokes. We could all just write untyped assembly instead and could do it safely by paying really good attention.
19. sanderjd ◴[] No.43751261{3}[source]
What do you mean by "they"? You mean the template interpolation functions?

Yes, the idea is that by having this in the language, library authors will write these implementations for use cases where they are appropriate.

replies(1): >>43751976 #
20. masklinn ◴[] No.43751285{3}[source]
Because t-strings don't create strings, so if the library doesn't support t-strings the call can just error.
21. thunky ◴[] No.43751449{8}[source]
Same is true if someone forgets to use t" and uses f" instead.

At least db.safe says what it does, unlike t".

replies(2): >>43751947 #>>43751995 #
22. ljm ◴[] No.43751841{4}[source]
I’d always prefer to use a prepared statement if I can, but sadly that’s also less feasible in the fancy new serverless execution environments where the DB adapter often can’t support them.

For me it just makes it easier to identify as safe, because it might not be obvious at a glance that an interpolated template string is properly sanitised.

23. ewidar ◴[] No.43751947{9}[source]
Not really, since f"" is a string and t"" is a template, you could make `db.execute` only accept templates, maybe have

`db.execute(Template)` and `db.unsafeExecute(str)`

replies(1): >>43755075 #
24. Tenoke ◴[] No.43751976{4}[source]
The sanitization. Just using a t-string in your old db.execute doesn't imply anything safer is going on than before.
replies(2): >>43752377 #>>43752677 #
25. fwip ◴[] No.43751995{9}[source]
Your linter can flag the type mismatch, and/or the function can reject f"" at runtime. This is because t"" yields a Template, not a str.

Template is also more powerful/concise in that the stringify function can handle the "formatting" args however it looks.

Note also, that there's no requirement that the template ever become a str to be used.

26. masklinn ◴[] No.43752377{5}[source]
Using a t-string in a db.execute which is not compatible with t-strings will result in an error.

Using a t-string in a db-execute which is, should be as safe as using external parameters. And using a non-t-string in that context should (eventually) be rejected.

replies(1): >>43752480 #
27. Tenoke ◴[] No.43752480{6}[source]
Again, just because a function accepts a t string it doesn't mean there's sanitization going on by default.
replies(1): >>43752636 #
28. tikhonj ◴[] No.43752636{7}[source]
Yes, but if a function accepts a template (which is a different type of object from a string!), either it is doing sanitization, or it explicitly implemented template support without doing sanitization—hard to do by accident!

The key point here is that a "t-string" isn't a string at all, it's a new kind of literal that's reusing string syntax to create Template objects. That's what makes this new feature fundamentally different from f-strings. Since it's a new type of object, libraries that accept strings will either have to handle it explicitly or raise a TypeError at runtime.

replies(1): >>43753556 #
29. nemetroid ◴[] No.43752677{5}[source]
Your "old" db.execute (which presumably accepts a regular old string) would not accept a t-string, because it's not a string. In the original example, it's a new db.execute.
30. Izkata ◴[] No.43753222{6}[source]
The first one already exists like:

  db.execute("SELECT * FROM table WHERE id = ?", (row_id,))
31. Tenoke ◴[] No.43753556{8}[source]
I'm not sure why you think it's harder to use them without sanitization - there is nothing inherent about checking the value in it, it's just a nice use.

You might have implemented the t-string to save the value or log it better or something and not even have thought to check or escape anything and definitely not everything (just how people forget to do that elsewhere).

replies(1): >>43753640 #
32. sanderjd ◴[] No.43753640{9}[source]
I really think you're misunderstanding the feature. If a method has a signature like:

    class DB:
        def execute(query: Template):
            ...
It would be weird for the implementation to just concatenate everything in the template together into a string without doing any processing of the template parameters. If you wanted an unprocessed string, you would just have the parameter be a string.
replies(1): >>43754238 #
33. dragonwriter ◴[] No.43754093{3}[source]
> and an extra function can confirm the safety of a value going into a f-string just as well.

Yes, you could require consumers to explicitly sanitize each parameter before it goes into the f-string, or, because it has the structure of what is fixed and what is parameters, it can do all of that for all parameters when it gets a t-string.

The latter is far more reliable, and you can't do it with an f-string because an f-string after creation is just a static string with no information about construction.

34. Tenoke ◴[] No.43754238{10}[source]
I'm not. Again, you might be processing the variable for logging or saving or passing elsewhere as well or many other reasons unrelated to sanitization.
replies(3): >>43754564 #>>43754600 #>>43755078 #
35. nemetroid ◴[] No.43754564{11}[source]
Sure, and the safe() function proposed upthread might also just be doing logging.
36. Ukv ◴[] No.43754600{11}[source]
The original comment said that it'd replace

    db.execute("QUERY WHERE name = ?", (name,))
with

    db.execute(t"QUERY WHERE name = {name}")
It's true that in theory `db.execute` could ignore semantics and concatenate together the template and variables to make a string without doing any sanitisation, but isn't the same true of the syntax it was claimed to replace?

Just because templates (or the previous syntax of passing in variables separately) could be used in a way that's equivalent safety-wise to an f-string by a poorly designed library does not mean that they add nothing over an f-string in general - they move the interpolation into db.execute where it can do its own sanitization and, realistically, sqlite3 and other libraries explicitly updated to take these will use it to do proper sanitization.

37. thunky ◴[] No.43755075{10}[source]
agreed. but then you're breaking the existing `db.execute(str)`. if you don't do that, and instead add `db.safe_execute(tpl: Template)`, then you're back to the risk that a user can forget to call the safe function.

also, you're trusting that the library implementer raises a runtime exception if a string a passed where a template is expected. it's not enough to rely on type-checks/linting. and there is probably going to be a temptation to accept `db.execute(sql: Union[str, Template])` because this is non-breaking, and sql without params doesn't need to be templated - so it's breaking some stuff that doesn't need to be broken.

i'm not saying templates aren't a good step forward, just that they're also susceptible to the same problems we have now if not used correctly.

replies(1): >>43759869 #
38. sanderjd ◴[] No.43755078{11}[source]
Taking a Template parameter into a database library's `execute` method is a big bright billboard level hint that the method is going to process the template parameters with the intent to make the query safe. The documentation will also describe the behavior.

You're right that the authors of such libraries could choose to do something different with the template parameter. But none of them will, for normal interface design reasons.

A library author could also write an implementation of a `plus` function on a numerical type that takes another numerical type, and return a string with the two numbers concatenated, rather than adding them together.

But nobody will do that, because libraries with extremely surprising behavior like that won't get used by anybody, and library authors don't want to write useless libraries. This is the same.

39. zahlman ◴[] No.43756889{3}[source]
> Then the useful part is the extra execute function you have to write

Well, no, the library author writes it. And the library author also gets to detect whether you pass a Template instance as expected, or (erroneously) a string created by whatever formatting method you choose. Having to use `safe(name)` within the f-string loses type information, and risks a greater variety of errors.

40. quinnirill ◴[] No.43759309{7}[source]
What does db.safe do though? How does it know what is the safe way of escaping at that point of the SQL? It will have no idea whether it’s going inside a string, if it’s in a field name position, denotes a value or a table name.

To illustrate the question further, consider a similar html.safe: f"<a href={html.safe(url)}>{html.safe(desc)</a>" - the two calls to html.safe require completely different escaping, how does it know which to apply?

41. ubercore ◴[] No.43759869{11}[source]
Then make `db.unsafe_execute` take a string.
replies(1): >>43761306 #
42. thunky ◴[] No.43761306{12}[source]
Yeah, you could. I'm just saying that by doing this you're breaking `db.execute` by not allowing it to take it string like it does now. Libraries may not want to add a breaking change for this.