Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Figure out robust pattern for read-only queries #16

Closed
simonw opened this issue Mar 14, 2021 · 4 comments
Closed

Figure out robust pattern for read-only queries #16

simonw opened this issue Mar 14, 2021 · 4 comments
Labels
documentation Improvements or additions to documentation enhancement New feature or request

Comments

@simonw
Copy link
Owner

simonw commented Mar 14, 2021

Split from #15 (comment)_

Dashboard queries should only execute read-only. There should be zero risk of a malicious /dashboard/?sql=update+blah query ever being executed.

The best way to do this is using a dedicated read-only PostgreSQL read-only role, see https://til.simonwillison.net/postgresql/read-only-postgresql-user

There's just one catch: on Heroku you need to pay at least $50/month to gain the ability to set up additional read-only roles! So ideally I'd like a working Heroku workaround here.

@simonw
Copy link
Owner Author

simonw commented Mar 14, 2021

How about using a separate database alias which sets that READ ONLY thing on every new connection? Something like this:

import psycopg2.extensions

DATABASES = {
    # ...
    'OPTIONS': {
        'isolation_level': psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE,
    },
}

https://docs.djangoproject.com/en/3.1/ref/databases/#postgresql-notes

https://nejc.saje.info/django-postgresql-readonly.html suggests:

'OPTIONS': {
    'options': '-c default_transaction_read_only=on'
}

@simonw
Copy link
Owner Author

simonw commented Mar 14, 2021

After much experimentation, I think I've found a good pattern for this - at least for PostgreSQL.

Setting '-c default_transaction_read_only=on' isn't quite good enough, because an attacker can construct their SQL like this:

SET TRANSACTION READ WRITE; update auth_user set is_superuser = true where username = 'demo';

But... SET TRANSACTION READ WRITE; only works if it's executed inside a transaction before any queries have been run.

So the patter that seems to work is:

  1. Operate against a connection with default_transaction_read_only=on
  2. Start a new transaction with BEGIN
  3. Run SELECT 1 - this prevents the user-provided SQL from running SET TRANSACTION READ WRITE;
  4. Now run the user-provided SQL
  5. Reset the state of the transaction with ROLLBACK at the end

I'd also like to prevent users from running multiple queries. The cheap way to do this is to ban any occurrences of a ; in the query, which I'm doing already, but I wonder if there's a more elegant way to do that?

if ";" in sql.rstrip(";"):
query_results.append(
{
"sql": sql,
"rows": [],
"description": [],
"truncated": False,
"error": "';' not allowed in SQL queries",
}
)

@simonw simonw added this to the First non-alpha release milestone Mar 14, 2021
@simonw
Copy link
Owner Author

simonw commented Mar 14, 2021

I can't find a better way to forbid multiple SQL queries than banning ; so I will keep on doing that.

@simonw simonw added documentation Improvements or additions to documentation enhancement New feature or request labels Mar 14, 2021
simonw added a commit that referenced this issue Mar 14, 2021
simonw added a commit that referenced this issue Mar 15, 2021
simonw added a commit that referenced this issue Mar 15, 2021
@simonw
Copy link
Owner Author

simonw commented Mar 16, 2021

I'm pleased with this, next step is to document it in #6.

@simonw simonw closed this as completed Mar 16, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant