diff --git a/edb/pgsql/resolver/static.py b/edb/pgsql/resolver/static.py index 5ee8fdaf131..6b9966eb84b 100644 --- a/edb/pgsql/resolver/static.py +++ b/edb/pgsql/resolver/static.py @@ -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( @@ -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) diff --git a/edb/tools/edb.py b/edb/tools/edb.py index 460d4465e98..b5da366c370 100644 --- a/edb/tools/edb.py +++ b/edb/tools/edb.py @@ -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 diff --git a/edb/tools/ls_forbidden_functions.py b/edb/tools/ls_forbidden_functions.py new file mode 100644 index 00000000000..d34c94e7969 --- /dev/null +++ b/edb/tools/ls_forbidden_functions.py @@ -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()) diff --git a/tests/test_sql_query.py b/tests/test_sql_query.py index 45be7014a68..d87eea39d60 100644 --- a/tests/test_sql_query.py +++ b/tests/test_sql_query.py @@ -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(