Skip to content

Commit

Permalink
Forbid certain system functions over SQL adapter (#7829)
Browse files Browse the repository at this point in the history
PostgreSQL supports certain SQL functions that allow [access to the
filesystem](https://www.postgresql.org/docs/16/functions-admin.html#FUNCTIONS-ADMIN-GENFILE),
managing backups, WAL and other administration functions of the
database.

This is not intended behavior of EdgeDB and goes against security model
of EdgeDB. To our knowledge, this feature is also not widely used by
users of PostgreSQL. Because of that, we are forbidding a range of
PostgreSQL system administration functions.

This change only affects access to EdgeDB via SQL adapter. Connections
directly to underlying PostgreSQL instance are not affected.
  • Loading branch information
aljazerzen authored Oct 4, 2024
1 parent a6b9408 commit b402d16
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 0 deletions.
89 changes: 89 additions & 0 deletions edb/pgsql/resolver/static.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,88 @@ def eval_TypeCast(
'pg_has_role': 2,
}

# Allowed functions from pg_catalog that start with `pg_`.
# By default, all such functions are forbidden by default.
# To see the list of forbidden functions, use `edb ls-forbidden-functions`.
ALLOWED_ADMIN_FUNCTIONS = frozenset(
{
'pg_is_in_recovery',
'pg_is_wal_replay_paused',
'pg_get_wal_replay_pause_state',
'pg_column_size',
'pg_column_compression',
'pg_database_size',
'pg_indexes_size',
'pg_relation_size',
'pg_size_bytes',
'pg_size_pretty',
'pg_table_size',
'pg_tablespace_size',
'pg_total_relation_size',
'pg_relation_filenode',
'pg_relation_filepath',
'pg_filenode_relation',
'pg_char_to_encoding',
'pg_column_is_updatable',
'pg_conf_load_time',
'pg_current_xact_id',
'pg_current_xact_id_if_assigned',
'pg_describe_object',
'pg_encoding_max_length',
'pg_encoding_to_char',
'pg_get_constraintdef',
'pg_get_expr',
'pg_get_function_arg_default',
'pg_get_function_arguments',
'pg_get_function_identity_arguments',
'pg_get_function_result',
'pg_get_functiondef',
'pg_get_indexdef',
'pg_get_keywords',
'pg_get_multixact_members',
'pg_get_object_address',
'pg_get_partition_constraintdef',
'pg_get_partkeydef',
'pg_get_publication_tables',
'pg_get_replica_identity_index',
'pg_get_replication_slots',
'pg_get_ruledef',
'pg_get_serial_sequence',
'pg_get_shmem_allocations',
'pg_get_statisticsobjdef',
'pg_get_triggerdef',
'pg_get_userbyid',
'pg_get_viewdef',
'pg_options_to_table',
'pg_has_role',
'pg_function_is_visible',
'pg_opclass_is_visible',
'pg_operator_is_visible',
'pg_opfamily_is_visible',
'pg_statistics_obj_is_visible',
'pg_table_is_visible',
'pg_ts_config_is_visible',
'pg_ts_dict_is_visible',
'pg_ts_parser_is_visible',
'pg_ts_template_is_visible',
'pg_type_is_visible',
'pg_index_column_has_property',
'pg_index_has_property',
'pg_is_in_backup',
'pg_is_other_temp_schema',
'pg_jit_available',
'pg_relation_is_updatable',
'pg_sequence_last_value',
'pg_sequence_parameters',
'pg_timezone_abbrevs',
'pg_timezone_names',
'pg_typeof',
'pg_visible_in_snapshot',
'pg_xact_commit_timestamp',
'pg_xact_status',
}
)


@eval.register
def eval_FuncCall(
Expand All @@ -153,6 +235,13 @@ def eval_FuncCall(
if not fn_name:
return None

if fn_name.startswith('pg_') and fn_name not in ALLOWED_ADMIN_FUNCTIONS:
raise errors.QueryError(
"forbidden function",
span=expr.span,
pgext_code=pgerror.ERROR_INSUFFICIENT_PRIVILEGE,
)

if fn_name == 'current_schemas':
return eval_current_schemas(expr, ctx=ctx)

Expand Down
1 change: 1 addition & 0 deletions edb/tools/edb.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,6 @@ def load_ext(args: tuple[str, ...]):
from . import gen_rust_ast # noqa
from . import ast_inheritance_graph # noqa
from . import parser_demo # noqa
from . import ls_forbidden_functions # noqa
from .profiling import cli as prof_cli # noqa
from .experimental_interpreter import edb_entry # noqa
66 changes: 66 additions & 0 deletions edb/tools/ls_forbidden_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#
# This source file is part of the EdgeDB open source project.
#
# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

from __future__ import annotations

import asyncio

from edb.tools.edb import edbcommands


async def run():
import asyncpg

# import os
# localdev = os.path.expanduser('~/.local/share/edgedb/_localdev')
# c = await asyncpg.connect(
# host=localdev, database='postgres', user='postgres'
# )

# docker run -it -p 5433:5432 --rm -e POSTGRES_PASSWORD=pass postgres:13
c = await asyncpg.connect(
host='localhost',
database='postgres',
user='postgres',
password='pass',
port='5433',
)

res = await c.fetch(
r'''
SELECT DISTINCT proname
FROM pg_proc p
INNER JOIN pg_namespace n ON pronamespace = n.oid
WHERE n.nspname = 'pg_catalog' AND proname like 'pg_%'
ORDER BY proname;
'''
)

import edb.pgsql.resolver.static as pg_r_static

print('Forbidden pg_* functions:')
for row in res:
[func_name] = row
if func_name in pg_r_static.ALLOWED_ADMIN_FUNCTIONS:
continue
print(' ', func_name)


@edbcommands.command("ls-forbidden-functions")
def main():
asyncio.run(run())
13 changes: 13 additions & 0 deletions tests/test_sql_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -1408,6 +1408,19 @@ async def test_sql_query_error_12(self):
res = await self.squery_values("SELECT 1")
self.assertEqual(res, [[1]])

async def test_sql_query_error_13(self):
# forbidden functions

with self.assertRaisesRegex(
asyncpg.InsufficientPrivilegeError,
'forbidden function',
position="8",
):
await self.scon.fetch("""SELECT pg_ls_dir('/')""")

res = await self.squery_values("""SELECT pg_is_in_recovery()""")
self.assertEqual(res, [[False]])

@unittest.skip("this test flakes: #5783")
async def test_sql_query_prepare_01(self):
await self.scon.execute(
Expand Down

0 comments on commit b402d16

Please sign in to comment.