Skip to content

[SUGGESTION] Change string interpolation result from a string to a lazy-evaluated parameterized-string #535

Description

@SecureFromScratch

**The idea:
For the interpolated string:
"select email from demo.useremails where username = (name)$ and type=(emailType)$"
Instead of interpolated strings generating the following cpp1 code:
"select email from demo.useremails where username = " + cpp2::to_String(name) + " and type=" + cpp2::to_string(emailType)
it will generate this code:
"select email from demo.useremails where username = " + cpp2::embed(name) + " and type=" + cpp2::embed(emailType)

This results in a parameterized_string type, which lazily holds the string literals and embedded arguments for later encoding and effectively implements the recommendation in LangSec revisited: input security flaws of the 2nd kind of delaying input sanitization and encoding to the output phase (where output encoding is known).

Will your feature suggestion eliminate X% of security vulnerabilities of a given kind in current C++ code?

This has the potential to transparently and implicitly reduce or eliminate the following Top 25 CWE categories (by rank):
(2) - CWE-79 - improperly neutralizing input when generating web pages (cross-site scripting)
(3) CWE-89 - improperly neutralizing special elements in SQL commands (SQL injection).
(6) CWE-78 - improperly neutralizing special elements in operating system commands (OS command injection).
(not-in-top25) [CWE-117] Improper Output Neutralization for Logs (log pollution/log forging)
to a lesser degree:
(8) CWE-22 - improperly limiting pathnames to restricted directories (path traversal)
(17) CWE-77 - improperly neutralizing special elements in commands (command injection).
(24) CWE-611 - improperly restricting XML external entity references.
(25) CWE-94 - improper control of code generation (code injection)

Note that these are all security vulnerabilities typically created from string concatenation with poorly sanitized/validated user input.

Describe alternatives you've considered.
Add a special interpolate-string prefix to control whether generated code creates a string or a parameterized-string, but this is not needed by simple adding an operator<< for parameterized strings.

Current methods are relying on programmers to be aware of the above mentioned CWEs and explicitly avoiding them.
By implementing lazy-concatenation and evaluation as interpolated strings default we remove the neccessity for programmer's awareness. Instead, the anus falls on library implementers to stop acceptting strings as input and instead support parameterized strings and perform appropriate validation,sanitization and encoding on them.

A good example is automatically generating a parameterized-sql-query from a parameterized string.

Another example is removing cr/lf characters when logging embedded arguments (but leave them for the string literals, thus supporting multiline log output but preventing unsanitized user input from causing log pollution or forgery)

Same goes for concatenating hard-coded html code with user supplied inputs, passing arguments to an os command by concatenating with user supplied inputs, etc.

**Change to cppfront:
The change to the cppfront is minimal: simply replace output of "cpp2::to_string" with "cpp2::embed" when handling interpolated strings and add an include file for "parameterized.hpp" which will contain the parameterized string implementation.

**POC

Checkout the fork at:
https://github.com/yarivtal/cppfront_parameterized_strings

"Example" folder contains both cpp2 samples and the generated cpp files (so you don't have to generate yourselfs)
The parameterized strings are implemented in a small library I created : pasteur
Its code is included in the fork for easier use, but you can find the lib here: https://github.com/SecureFromScratch/pasteur

Activity

  1. JohelEGP commented on Jul 4, 2023

    @JohelEGP
    Contributor

    This feels like the wrong level for security.

    First, currently, an interpolated string literal results in a std::string.
    It reads as if you're suggesting to delay such evaluation.
    You suggest that solves security vulnerabilities.
    But it seems to me that, in turn, you're introducing
    the same security vulnerabilities that come with using lambdas that capture by reference.

    Second, I think sanitation should be orthogonal to concatenation.
    You should probably have some unsanitized_𝘴𝘰𝘮𝘦𝘵𝘩𝘪𝘯𝘨 types,
    sanitized_𝘴𝘰𝘮𝘦𝘵𝘩𝘪𝘯𝘨 sanitize(unsanitized_𝘴𝘰𝘮𝘦𝘵𝘩𝘪𝘯𝘨) functions,
    and only have sanitized_𝘴𝘰𝘮𝘦𝘵𝘩𝘪𝘯𝘨 be convertible to std::string.

  2. msadeqhe commented on Jul 5, 2023

    @msadeqhe

    Lazy-evaluated interpolated strings would be like capture by reference within string literals.

    Consider if we could use (pointer)$* to mean it:

    x: = 10;
    
    // cpp2::to_string(x)
    a: = "(x)$";
    
    // lazy-evaluated, capture by reference
    // cpp2::embed(x&)*
    b: = "(x&)$*";
  3. MaxSagebaum commented on Jul 5, 2023

    @MaxSagebaum
    Contributor

    I think the concept of capture by reference in string interpolation is problematic. You introduce functions with side effects which can be overlooked quite easily, thus introducing new errors.

    If you want to create lazy interpolation strings, then I would expect a function object which is then called accordingly:

    x := "select email from demo.useremails where username = (name)$ and type=(emailType)$"
    querry(x("my name", "work"));
    // or
    querry("my name", "work", x);
    
  4. JohelEGP commented on Jul 5, 2023

    @JohelEGP
    Contributor

    I was thinking of just using a function expression,
    which is our best approximation of lazy arguments.

  5. SecureFromScratch commented on Jul 5, 2023

    @SecureFromScratch
    Author

    Thanks for the feedback. I'll try to answer all of the points given.

    Wrong Level of security: Sanitization of input is NOT a solution, and is in fact an anti-pattern, since it assumes we know at the point of input what sanitization will suffice for output even though we usually have multiple output sinks for the same input, i.e. db, log, screen, network.
    You could argue that this is a programmer's education problem, but this was also said of pointers and manual memory management... and injection is still #3 on owasp top 10 and on the top 10 cwe. Language support seems crucial.
    I agree about the "unsanitized" types, and I actually use something similar, but suggesting that seems like too big of a chang and in any case more in the area of library writers than the language. In any case - these unsanitized/sanitized types will not prevent unknowledgeable programmers from using string concatenation (an anti-pattern!) but string interpolation is simple enough and convenient enough to actually be used instead of string-concatenation.

    Introducing the same security vulnerabilities that come with using lambdas that capture by reference: my intent is actually copying values, in assumption that in most cases this is no more intensive than generating a string with to_string. However, after the remarks given here I suppose support for move and by_ref will need to be given.

    I was thinking of just using a function expression: Sounds like a great idea for capturing without additional library constructs like my parameterized_string. To be honest, I wrote the parameterized_string simply for POC purposes and then thought "why not use it in other places"...
    I assume the generated function would then receive some sort of renderer callback(s) that would be called per literal/argument?

  6. SecureFromScratch commented on Jul 5, 2023

    @SecureFromScratch
    Author

    @MaxSagebaum I now see I misunderstood the meaning of your suggestion for a function call - you suggested generating a lazy evaluated binding as a function that later accepts input parameters and fills in the blanks - this is similar to std::format/{fmt}.
    However, my meaning is that the binding is not lazy - only it's interpretation or rendering as a concatenated string is lazy - that is delayed until the point where the form of the output is known.
    This allows for sql libraries to render it as a parameterized query, html libraries to sanitize for html tags and encode it, log libraries to remove cr/lf and other polluting characters, etc...

    So, if it were to be a function than it would be a function accepting a renderer object or a pair of callbacks.

  7. MaxSagebaum commented on Jul 6, 2023

    @MaxSagebaum
    Contributor

    Ok, now I understand it better.

    I doubt, that a new string class will be immediately used everywhere. It needs to be introduced in the libraries and users need to be made aware of it. If you forbid the automatic cast to std::string a lot of errors will be generated. In most cases the user will use the available to_string() method. This will be one feature you can advertise as input arguments no longer need to be sanitized, the backend will do this. People will then thin, ok I do not need to do the checking of my inputs any longer and the problem will become worse.

    It might be nice to have such a thing, but it might send a counterproductive signal.

  8. SecureFromScratch commented on Jul 6, 2023

    @SecureFromScratch
    Author

    The question is: why would you need a conversion to string?
    Since this is the result of what would normally be string concatenation I can think of 4 scenarios (if you have more, please tell me):

    1. You want to output to a stream - supported by operator<<(ostream&, const paramaterized_string<...>&)
    2. You want to concatenate with more arguments/literals. Don't need a string for that - it's supported by parameterized_String as is.
    3. You want to pass to a library function that does something with the string as data (i.e. exceute sql)- with a string type this is unsafe since strings used as data are by their nature unparsed data and their concatenation can (and will) created unpredicatable legal syntax that the parser will accept. This exactly when you would like to define a rendered that breaks the literals+arguments into components and calls appropriate functions to handle each differently.

    BTW Conversion to string is always possible via stringstream, it's just unconvinient. This is intentional - to make writing dangerous code longer/more verbose so it isn't the first option taken.

    And again - input arguments are impossible to correctly sanitize since you cannot predict what output they will be used for (attempting to match the sanitization to the expected output sinks creates brittle coupling between input sanitization and output). (BTW, this is contrast to validation which is definitely possible and a must)

  9. JohelEGP commented on Jul 6, 2023

    @JohelEGP
    Contributor

    But it seems to me that, in turn, you're introducing
    the same security vulnerabilities that come with using lambdas that capture by reference.

    This is somewhat alleviated by Cpp2 not having default captures.
    In Cpp2, all captures are explicit.

  10. SecureFromScratch commented on Jul 14, 2023

    @SecureFromScratch
    Author

    How about starting with supporting programmer explicit buy-in of this feature via a string prefix? i.e.:
    f"select email from demo.useremails where username = (name)$ and type=(emailType)$"?
    (f is chosen here due to it's familiarity from python formatted strings).
    Or a compiler flag to allow experimenting with the feature?

  11. Repository owner locked and limited conversation to collaborators on Aug 30, 2023
  12. converted this issue into a discussion #627 on Aug 30, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions