4

I'm working on a typical client-server webapp. It is using a system somewhat like GraphQL where the client has some flexibility in specifying what data it needs, without custom API endpoints for every type of data. The server is running node, and is using node-postgres with a typical pg.Pool. A client could send something like this:

{select: '*', from: 'expenses', where: {'op': 'gt', 'lhs': 'expenses.amount', 'rhs': 20}}

which would be translated to SELECT * FROM expenses WHERE expenses.amount > $1 (given $1 = 20). With enough care, this system can be made safe from injection attacks.

I'd also like to incorporate row-level security policies. For example:

create policy only_see_own_expenses on expenses using (expenses.user_id = <USER ID>);

As an extra security barrier, I want to make sure that even if an injection attack is succesful, a client can not "unset" its user ID.

I've seen <USER ID> been defined in a few ways:

  1. current_user, in which case every user of the app also needs a postgres user/role
  2. An arbitrary setting like current_setting('myapp.user_id') in combination with a SET LOCAL myapp.user_id = ... at the start of a transaction

Approach (2) seems most flexible to me. I'd just wrap every generated SQL query in a BEGIN; SET LOCAL myapp.user_id = 123; {generated query}; END;. The problem is that an attacker could inject another SET LOCAL statement, and impersonate another user.

In approach (1) you can similarly wrap every generated query with a SET ROLE ... statement at the start, yielding the same problem. An alternative is to create a new connection for each query with that specific role. I believe postgres would never allow that connection to switch to another role. But setting up a new connection per query would result in a lot of overhead.

How do I enforce row-level security without the performance hit of a new connection per query?

1
  • 1
    "I believe postgres would never allow that connection to switch to another role." - it does, actually. You need to distinguish the authentication role (that the connection was opened with), the session user identifier (which a superuser can impersonate), and the current user identifier that can be set to any role granted to the session user. So unless you create a separate login role for each user, an SQL injection could switch to an arbitrary role. Commented Oct 11, 2023 at 15:59

2 Answers 2

3

As you observe, setting a placeholder parameter and using SET LOCAL ROLE to temporarily assume a different role can be subverted by attackers who can execute arbitrary SQL, like in an SQL injection attack.

I don't think that there is a way to accomplish what you want that is safe from SQL injection. The problem is a fundamental one: you handle authentication in the application, not in the database (where you are using a single application user to benefit from connection pooling), but you want to have the database handle authorization via row-level security. That requires a way for the application to tell the database what the application user is. Now the only way that the application can tell the database anything is by using SQL, and an attacker who can run arbitrary SQL statements can always subvert that.

I think that the only option for you is to harden your application against SQL injection attacks.

Sign up to request clarification or add additional context in comments.

Comments

1
+50

As an extra security barrier, I want to make sure that even if an injection attack is succesful, a client can not "unset" its user ID.

Your second solution is not only perfectly viable, it's exactly what many websites do. Sure, anyone can edit the user_id parameter in a cookie, URL, SQL statement or even custom protocol frame, but as long as you do not

  1. expose the list of valid identifiers
  2. make the identifiers easy/possible to guess/brute-force through
  3. share or re-use them

changing it just makes no sense because you have nothing to change it to. If someone just "unsets" the id, it'll break their access. Same if they set it wrong. Your only concern is to make sure if they set it right, there's no way they got the other id from you. Assuming the above, having another token to change to means it was acquired outside the system, beyond your control (given, stolen, leaked - not by you), or that it's just multiple sessions of the same person and they have that access anyways.

That being said, by no means does it free you from the necessity to also lock down your SQL against injections and it kind of requires you to set up and manage this token-based authentication on top of everything else, which might be a challenge on its own. Make sure you know how rules and privileges work and that you're not leaking anything through security definer routines or views with security_barrier off.

9 Comments

…or security definer views (ie. with security_invoker off, which is the default!)
Do I misunderstand, or are you advocating "security by obscurity"?
@LaurenzAlbe I'm not but I'm interested in how you got that impression
"As long as you do not make the identifiers easy/possible to guess/brute-force changing it just makes no sense because you have nothing to change it to." To find out if a certain UUID exists in a table that you cannot read owing to RLS, can't you just EXPLAIN (ANALYZE) a query and see if there was a row removed by a filter?
By all means, you can. But why? If you have a valid token, you don't need explain analyze to tell you that - if you're trying to test tokens in a brute-force attempt, that's just a slower way to do so. You could even load heavy batches of uuid guesses all day up to the 1gig but it's worth reminding how sparse even regular uuids are and that in such scenario it is no longer a break-out attempt, it's a denial of service. It's not very different from any other password guessing game.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.