pax_global_header00006660000000000000000000000064152234647620014525gustar00rootroot0000000000000052 comment=729f8bf99a38d43004750d8debfd28de2fca936d dimitri-pgextwlist-ab92a4c/000077500000000000000000000000001522346476200160655ustar00rootroot00000000000000dimitri-pgextwlist-ab92a4c/.editorconfig000066400000000000000000000010031522346476200205340ustar00rootroot00000000000000# top-most EditorConfig file root = true # rules for all files # we use tabs with indent size 4 [*] indent_style = tab indent_size = 4 tab_width = 4 end_of_line = lf insert_final_newline = true charset = utf-8 trim_trailing_whitespace = true # Don't change test output files, pngs or test data files [*.{out,png,data}] insert_final_newline = unset trim_trailing_whitespace = unset [*.{sql,sh,py,tex}] indent_style = space indent_size = 4 tab_width = 4 [*.yml] indent_style = space indent_size = 2 tab_width = 2 dimitri-pgextwlist-ab92a4c/.github/000077500000000000000000000000001522346476200174255ustar00rootroot00000000000000dimitri-pgextwlist-ab92a4c/.github/workflows/000077500000000000000000000000001522346476200214625ustar00rootroot00000000000000dimitri-pgextwlist-ab92a4c/.github/workflows/regression.yml000066400000000000000000000011451522346476200243660ustar00rootroot00000000000000name: make installcheck on: [push, pull_request] jobs: test: strategy: matrix: pg: - 20 - 19 - 18 - 17 - 16 - 15 - 14 - 13 - 12 - 11 - 10 name: PostgreSQL ${{ matrix.pg }} runs-on: ubuntu-latest container: pgxn/pgxn-tools steps: - name: Start PostgreSQL ${{ matrix.pg }} run: pg-start ${{ matrix.pg }} - name: Check out the repo uses: actions/checkout@main - name: Test on PostgreSQL ${{ matrix.pg }} run: pg-build-test dimitri-pgextwlist-ab92a4c/.gitignore000066400000000000000000000000741522346476200200560ustar00rootroot00000000000000*.bc *.o pgextwlist.so regression.* results/ .deps .vagrant dimitri-pgextwlist-ab92a4c/Makefile000066400000000000000000000023071522346476200175270ustar00rootroot00000000000000short_ver = $(shell git describe --match "v[0-9]*" --abbrev=4 HEAD | cut -d- -f1 | sed 's/^v//') long_ver = $(shell (git describe --tags --long '--match=v*' 2>/dev/null || echo $(short_ver)-0-unknown) | cut -c2-) MODULE_big = pgextwlist OBJS = utils.o pgextwlist.o DOCS = README.md REGRESS = setup pgextwlist errors crossuser hooks path_validation substitution_safety restrict_to_owner RPM_MINOR_VERSION_SUFFIX ?= PG_CONFIG = pg_config PGXS = $(shell $(PG_CONFIG) --pgxs) include $(PGXS) DEBUILD_ROOT = /tmp/pgextwlist deb: mkdir -p $(DEBUILD_ROOT) && rm -rf $(DEBUILD_ROOT)/* rsync -Ca --exclude=build/* ./ $(DEBUILD_ROOT)/ cd $(DEBUILD_ROOT) && make -f debian/rules orig cd $(DEBUILD_ROOT) && debuild -us -uc -sa cp -a /tmp/pgextwlist_* /tmp/postgresql-9.* build/ rpm: git archive --output=pgextwlist-rpm-src.tar.gz --prefix=pgextwlist/ HEAD rpmbuild -bb pgextwlist.spec \ --define '_sourcedir $(CURDIR)' \ --define 'package_prefix $(package_prefix)' \ --define 'pkglibdir $(shell $(PG_CONFIG) --pkglibdir)' \ --define 'major_version $(short_ver)' \ --define 'minor_version $(subst -,.,$(subst $(short_ver)-,,$(long_ver)))$(RPM_MINOR_VERSION_SUFFIX)' $(RM) pgextwlist-rpm-src.tar.gz dimitri-pgextwlist-ab92a4c/README.md000066400000000000000000000213351522346476200173500ustar00rootroot00000000000000# PostgreSQL Extension Whitelist This extension implements extension whitelisting, and will actively prevent users from installing extensions not in the provided list. Also, this extension implements a form of `sudo` facility in that the whitelisted extensions will get installed as if *superuser*. Privileges are dropped before handing the control back to the user. The operations `CREATE EXTENSION`, `DROP EXTENSION`, `ALTER EXTENSION ... UPDATE`, and `COMMENT ON EXTENSION` are run by *superuser*. The `ALTER EXTENSION ... ADD|DROP` command is intentionally not supported so as not to allow users to modify an already installed extension. That means that it's not currently possible to `CREATE EXTENSION ... FROM 'unpackaged';`. Note that the extension script is running as if run by a stored procedure owned by your *bootstrap superuser* and with `SECURITY DEFINER`, meaning that the extension and all its objects are owned by this *superuser*. PostgreSQL versions 10 and later are supported. ## Licence The `pgextwlist` PostgreSQL extension is released under [The PostgreSQL Licence](http://www.postgresql.org/about/licence/), a liberal Open Source license, similar to the BSD or MIT licenses. ## Install 1. Install the server development packages (on Ubuntu, this would look like `apt-get install postgresql-server-dev-all`) 2. then: make sudo make install This will generate a `pgextwlist.so` shared library that you will have to install in `pg_config --pkglibdir`/plugins so that your backend loads it automatically. ## Setup You need to define the list of extensions that are whitelisted, the user that performs the extension installing, and the error behavior. * `local_preload_libraries` Add `pgextwlist` to the `local_preload_libraries` setting. Don't forget to add the module in the `$plugin` directory. * `extwlist.extensions` List of extensions allowed for installation. To allow only certain users to use the whitelist, use `ALTER ROLE` instead of setting this parameter globally: `ALTER ROLE adminuser SET extwlist.extensions = 'pg_stat_statements, postgis';` * `extwlist.custom_path` Filesystem path where to look for *custom scripts*. Must point to an existing, readable directory; misconfiguration raises an `ERROR` at assignment time (or at `CREATE` / `UPDATE EXTENSION` time for the per-extension subdirectory) — scripts are never silently skipped. An empty string disables the feature. * `extwlist.restrict_to_database_owner` When `on`, pgextwlist only invokes its superuser-override path (`CREATE`, `ALTER … UPDATE`, `DROP`, `COMMENT ON EXTENSION` on whitelisted extensions) when the current user owns the current database. Non-owners receive `ERROR: extension "…" … via pgextwlist is restricted to the database owner`. Trusted extensions and any non-whitelisted extension fall through to PostgreSQL's own checks unchanged. Off by default. ## Usage That's quite simple: $ edit postgresql.conf to add local_preload_libraries, custom_variable_classes and extwlist.extensions dim=# show extwlist.extensions; show extwlist.extensions; extwlist.extensions --------------------- hstore,cube (1 row) dim=# create extension foo; create extension foo; ERROR: extension "foo" is not whitelisted DETAIL: Installing the extension "foo" failed, because it is not on the whitelist of user-installable extensions. HINT: Your system administrator has allowed users to install certain extensions. See: SHOW extwlist.extensions; dim=# create extension hstore; create extension hstore; WARNING: => is deprecated as an operator name DETAIL: This name may be disallowed altogether in future versions of PostgreSQL. CREATE EXTENSION dim=# \dx \dx List of installed extensions Name | Version | Schema | Description ---------+---------+------------+-------------------------------------------------- hstore | 1.0 | public | data type for storing sets of (key, value) pairs plpgsql | 1.0 | pg_catalog | PL/pgSQL procedural language (2 rows) Even if you're not superuser: dim=> select rolsuper from pg_roles where rolname = current_user; select rolsuper from pg_roles where rolname = current_user; rolsuper ---------- f (1 row) dim=> create extension hstore; create extension hstore; WARNING: => is deprecated as an operator name DETAIL: This name may be disallowed altogether in future versions of PostgreSQL. CREATE EXTENSION dim=> create extension earthdistance; create extension earthdistance; ERROR: extension "earthdistance" is not whitelisted DETAIL: Installing the extension "earthdistance" failed, because it is not on the whitelist of user-installable extensions. HINT: Your system administrator has allowed users to install certain extensions. SHOW extwlist.extensions; dim=> \dx \dx List of installed extensions Name | Version | Schema | Description ---------+---------+------------+-------------------------------------------------- hstore | 1.0 | public | data type for storing sets of (key, value) pairs plpgsql | 1.0 | pg_catalog | PL/pgSQL procedural language (2 rows) dim=> drop extension hstore; drop extension hstore; DROP EXTENSION ## Custom Scripts Some extensions are installing objects that only the *superuser* can make use of by default, it's then a good idea to tweak permissions and grant usage to the *current_user* or even the *database owner*, depending. The custom scripts feature allows to do that by providing scripts to be run around the execution of the extension's script itself. #### `create extension` custom scripts For the creation of extension `extname` version `1.0` the following scripts will be used when they do exist, as shown here: - `${extwlist.custom_path}/extname/before--1.0.sql` - `${extwlist.custom_path}/extname/before-create.sql`, only when the previous one, specific to the version being installed, does not exists. - The `CREATE EXTENSION` command now runs normally - `${extwlist.custom_path}/extname/after--1.0.sql` - `${extwlist.custom_path}/extname/after-create.sql`, only when the specific one does not exist #### `alter extension update` custom scripts For the update of extension `extname` from version `1.0` to version `1.1` the following scripts will be used when they do exist, as shown here: - `${extwlist.custom_path}/extname/before--1.0--1.1.sql` - `${extwlist.custom_path}/extname/before-update.sql`, only when the specific one does not exists. - The `ALTER EXTENSION UPDATE` command now runs normally - `${extwlist.custom_path}/extname/after--1.0--1.1.sql` - `${extwlist.custom_path}/extname/after-update.sql` only when the specific one does not exists. #### `comment on extension` and `drop extension` scripts Similarly: - `${extwlist.custom_path}/extname/before-comment.sql` - `${extwlist.custom_path}/extname/after-comment.sql` - `${extwlist.custom_path}/extname/before-drop.sql` - `${extwlist.custom_path}/extname/after-drop.sql` Version-specific hook files are not supported here. #### custom scripts templating Before executing them, the *extwlist* extension applies the following substitutions to the *custom scripts*: - any line that begins with `\echo` is removed, - the literal `@extschema@` is unconditionally replaced by the current schema being used to create the extension objects, - the literal `@current_user@` is replaced by the name of the current user, - the literal `@database_owner@` is replaced by the name of the current database owner. Tip: remember that you can execute `DO` blocks if you need dynamic SQL. ## Internals The whitelisting works by overloading the `ProcessUtility_hook` and gaining control each time a utility statement is issued. When this statement is a `CREATE EXTENSION`, the extension's name is extracted from the `parsetree` and checked against the whitelist. *Superuser* is obtained as in the usual `SECURITY DEFINER` case, except hard coded to target the *bootstrap user*. ## Maintenance & Support This project is maintained through [OSS Maintenance](https://oss.theartofpostgresql.com) — a funded model designed for **cloud providers and enterprises** running pgextwlist in production. - Priority bug fixes and improvements via a paid queue - Quarterly releases with predictable delivery - Upstream-first: fixes land in the project itself PostgreSQL consultancies can also subscribe at the **Partner** tier to cover their entire client base under a single plan. **[View support plans](https://oss.theartofpostgresql.com/#pricing)** dimitri-pgextwlist-ab92a4c/expected/000077500000000000000000000000001522346476200176665ustar00rootroot00000000000000dimitri-pgextwlist-ab92a4c/expected/crossuser.out000066400000000000000000000010301522346476200224410ustar00rootroot00000000000000GRANT USAGE, CREATE ON SCHEMA public TO public; DO LANGUAGE plpgsql $$ BEGIN CREATE ROLE evil_user; EXCEPTION WHEN duplicate_object THEN NULL; END $$; SET ROLE mere_mortal; CREATE TABLE mere_table (t citext); SET ROLE evil_user; DROP EXTENSION citext; ERROR: cannot drop extension citext because other objects depend on it DETAIL: table mere_table column t depends on type citext HINT: Use DROP ... CASCADE to drop the dependent objects too. DROP EXTENSION citext CASCADE; NOTICE: drop cascades to table mere_table column t dimitri-pgextwlist-ab92a4c/expected/crossuser_1.out000066400000000000000000000010361522346476200226670ustar00rootroot00000000000000GRANT USAGE, CREATE ON SCHEMA public TO public; DO LANGUAGE plpgsql $$ BEGIN CREATE ROLE evil_user; EXCEPTION WHEN duplicate_object THEN NULL; END $$; SET ROLE mere_mortal; CREATE TABLE mere_table (t citext); SET ROLE evil_user; DROP EXTENSION citext; ERROR: cannot drop extension citext because other objects depend on it DETAIL: column t of table mere_table depends on type citext HINT: Use DROP ... CASCADE to drop the dependent objects too. DROP EXTENSION citext CASCADE; NOTICE: drop cascades to column t of table mere_table dimitri-pgextwlist-ab92a4c/expected/errors.out000066400000000000000000000027611522346476200217410ustar00rootroot00000000000000SELECT case when setting::int >= 130000 then 'PG 13+' when setting::int >= 100000 then 'PG 10..12' end as "regression output for PG version" FROM pg_settings where name = 'server_version_num'; regression output for PG version ---------------------------------- PG 10..12 (1 row) SET ROLE mere_mortal; SELECT extname FROM pg_extension ORDER BY 1; extname --------- citext plpgsql (2 rows) -- pre-existing extension CREATE EXTENSION plpgsql; ERROR: extension "plpgsql" already exists COMMENT ON EXTENSION hstore IS 'plpgsql comment'; ERROR: extension "hstore" does not exist SELECT extname FROM pg_extension ORDER BY 1; extname --------- citext plpgsql (2 rows) -- non-whitelisted extension CREATE EXTENSION hstore; ERROR: permission denied to create extension "hstore" HINT: Must be superuser to create this extension. COMMENT ON EXTENSION hstore IS 'hstore comment'; ERROR: extension "hstore" does not exist SELECT extname FROM pg_extension ORDER BY 1; extname --------- citext plpgsql (2 rows) -- whitelisted extension, but dependency is missing CREATE EXTENSION earthdistance; ERROR: required extension "cube" is not installed HINT: Use CREATE EXTENSION ... CASCADE to install required extensions too. SELECT extname FROM pg_extension ORDER BY 1; extname --------- citext plpgsql (2 rows) -- drop non-whitelisted extension DROP EXTENSION plpgsql; ERROR: must be owner of extension plpgsql SELECT extname FROM pg_extension ORDER BY 1; extname --------- citext plpgsql (2 rows) dimitri-pgextwlist-ab92a4c/expected/errors_1.out000066400000000000000000000030131522346476200221500ustar00rootroot00000000000000SELECT case when setting::int >= 130000 then 'PG 13+' when setting::int >= 100000 then 'PG 10..12' end as "regression output for PG version" FROM pg_settings where name = 'server_version_num'; regression output for PG version ---------------------------------- PG 13+ (1 row) SET ROLE mere_mortal; SELECT extname FROM pg_extension ORDER BY 1; extname --------- citext plpgsql (2 rows) -- pre-existing extension CREATE EXTENSION plpgsql; ERROR: extension "plpgsql" already exists COMMENT ON EXTENSION hstore IS 'plpgsql comment'; ERROR: extension "hstore" does not exist SELECT extname FROM pg_extension ORDER BY 1; extname --------- citext plpgsql (2 rows) -- non-whitelisted extension CREATE EXTENSION hstore; ERROR: permission denied to create extension "hstore" HINT: Must have CREATE privilege on current database to create this extension. COMMENT ON EXTENSION hstore IS 'hstore comment'; ERROR: extension "hstore" does not exist SELECT extname FROM pg_extension ORDER BY 1; extname --------- citext plpgsql (2 rows) -- whitelisted extension, but dependency is missing CREATE EXTENSION earthdistance; ERROR: required extension "cube" is not installed HINT: Use CREATE EXTENSION ... CASCADE to install required extensions too. SELECT extname FROM pg_extension ORDER BY 1; extname --------- citext plpgsql (2 rows) -- drop non-whitelisted extension DROP EXTENSION plpgsql; ERROR: must be owner of extension plpgsql SELECT extname FROM pg_extension ORDER BY 1; extname --------- citext plpgsql (2 rows) dimitri-pgextwlist-ab92a4c/expected/hooks.out000066400000000000000000000012671522346476200215500ustar00rootroot00000000000000select case when setting::int >= 160000 then 'PG 16+' end as "regression output for PG version" from pg_settings where name = 'server_version_num'; regression output for PG version ---------------------------------- (1 row) set client_min_messages = debug; set extwlist.custom_path = ''; set role mere_mortal; -- in the hope refint stays at version 1.0 in PostgreSQL create extension refint; alter extension refint update; NOTICE: version "1.0" of extension "refint" is already installed comment on extension refint is 'snarky remark'; drop extension refint, refint; DEBUG: drop auto-cascades to function check_primary_key() DEBUG: drop auto-cascades to function check_foreign_key() dimitri-pgextwlist-ab92a4c/expected/hooks_1.out000066400000000000000000000013731522346476200217660ustar00rootroot00000000000000select case when setting::int >= 160000 then 'PG 16+' end as "regression output for PG version" from pg_settings where name = 'server_version_num'; regression output for PG version ---------------------------------- PG 16+ (1 row) set client_min_messages = debug; set extwlist.custom_path = ''; set role mere_mortal; -- in the hope refint stays at version 1.0 in PostgreSQL create extension refint; DEBUG: executing extension script for "refint" version '1.0' alter extension refint update; NOTICE: version "1.0" of extension "refint" is already installed comment on extension refint is 'snarky remark'; drop extension refint, refint; DEBUG: drop auto-cascades to function check_primary_key() DEBUG: drop auto-cascades to function check_foreign_key() dimitri-pgextwlist-ab92a4c/expected/path_validation.out000066400000000000000000000041741522346476200235730ustar00rootroot00000000000000-- extwlist.custom_path validation -- -- The configured path must exist and be a readable directory; an -- unreadable or missing top-level path is a hard ERROR to avoid silently -- bypassing the extension whitelist via filesystem misconfiguration. \set goodpath `pwd` '/test-scripts' \set regfile '/etc/passwd' -- 1. Nonexistent path is rejected at SET time. SET extwlist.custom_path = '/no/such/directory/pgextwlist-test'; ERROR: invalid value for parameter "extwlist.custom_path": could not access "/no/such/directory/pgextwlist-test": No such file or directory -- Value should be unchanged after rejection (still the cluster default). SELECT substring(setting FROM '/([^/]*)$') AS suffix FROM pg_settings WHERE name = 'extwlist.custom_path'; suffix -------------- test-scripts (1 row) -- 2. A regular file is rejected (not a directory). SET extwlist.custom_path = :'regfile'; ERROR: invalid value for parameter "extwlist.custom_path": "/etc/passwd" is not a directory SELECT substring(setting FROM '/([^/]*)$') AS suffix FROM pg_settings WHERE name = 'extwlist.custom_path'; suffix -------------- test-scripts (1 row) -- 3. Empty string disables the feature (no validation, no script lookup). SET extwlist.custom_path = ''; SELECT setting AS suffix FROM pg_settings WHERE name = 'extwlist.custom_path'; suffix -------- (1 row) -- With feature off, CREATE EXTENSION on a whitelisted extension succeeds -- even though no custom_path is configured. SET ROLE mere_mortal; CREATE EXTENSION citext; DROP EXTENSION citext; RESET ROLE; -- 4. Setting back to a valid directory is accepted. SET extwlist.custom_path = :'goodpath'; SELECT substring(setting FROM '/([^/]*)$') AS suffix FROM pg_settings WHERE name = 'extwlist.custom_path'; suffix -------------- test-scripts (1 row) -- Per-extension subdirectory absent is a silent skip: CREATE EXTENSION -- on a whitelisted extension whose subdir does not exist under the -- custom_path must succeed without error. SET ROLE mere_mortal; CREATE EXTENSION refint; DROP EXTENSION refint; RESET ROLE; -- 5. Restore default so downstream tests start from a known state. RESET extwlist.custom_path; dimitri-pgextwlist-ab92a4c/expected/pgextwlist.out000066400000000000000000000030551522346476200226340ustar00rootroot00000000000000SET ROLE mere_mortal; SHOW extwlist.extensions; extwlist.extensions -------------------------------------------------------- citext,earthdistance,pg_trgm,pg_stat_statements,refint (1 row) SELECT extname FROM pg_extension ORDER BY 1; extname --------- plpgsql (1 row) -- whitelisted extensions CREATE EXTENSION citext; CREATE EXTENSION pg_trgm; COMMENT ON EXTENSION pg_trgm IS 'pg_trgm comment'; SELECT extname FROM pg_extension ORDER BY 1; extname --------- citext pg_trgm plpgsql (3 rows) SELECT d.description FROM pg_extension e JOIN pg_description d ON d.objoid = e.oid WHERE e.extname = 'pg_trgm'; description ----------------- pg_trgm comment (1 row) -- drop whitelisted extension DROP EXTENSION pg_trgm; SELECT extname FROM pg_extension ORDER BY 1; extname --------- citext plpgsql (2 rows) -- whitelisted extension with custom scripts CREATE EXTENSION pg_stat_statements; SELECT extname FROM pg_extension ORDER BY 1; extname -------------------- citext pg_stat_statements plpgsql (3 rows) SELECT proacl FROM pg_proc WHERE proname = 'pg_stat_statements_reset'; proacl ---------------------------------------------------------- {mere_mortal=X/mere_mortal,stat_resetters=X/mere_mortal} (1 row) SELECT groname FROM pg_group WHERE groname = 'stat_resetters'; groname ---------------- stat_resetters (1 row) DROP EXTENSION pg_stat_statements; SELECT groname FROM pg_group WHERE groname = 'stat_resetters'; groname --------- (0 rows) dimitri-pgextwlist-ab92a4c/expected/restrict_to_owner.out000066400000000000000000000106651522346476200242020ustar00rootroot00000000000000-- extwlist.restrict_to_database_owner -- -- When this GUC is on, pgextwlist only invokes its superuser-override path -- (CREATE / ALTER UPDATE / DROP / COMMENT EXTENSION on whitelisted -- extensions) when the current user is the database owner. Non-owners -- receive an explicit error. Trusted extensions and non-whitelisted -- extensions fall through to PG's normal checks. -- The pg_regress connection user owns contrib_regression in installcheck -- mode, so that user IS the database owner for these tests. -- Reset cluster-wide state that might be left over from prior runs. DROP EXTENSION IF EXISTS pg_stat_statements; NOTICE: extension "pg_stat_statements" does not exist, skipping DROP ROLE IF EXISTS stat_resetters; NOTICE: role "stat_resetters" does not exist, skipping DROP EXTENSION IF EXISTS btree_gin; NOTICE: extension "btree_gin" does not exist, skipping -- ===================================================================== -- 1. Default (GUC off) preserves current behavior. -- ===================================================================== SET ROLE mere_mortal; CREATE EXTENSION pg_stat_statements; RESET ROLE; DROP EXTENSION pg_stat_statements; DROP ROLE stat_resetters; -- ===================================================================== -- 2. GUC on, non-DB-owner (mere_mortal): CREATE on whitelisted ext is -- blocked by pgextwlist with an explicit error. -- ===================================================================== SET extwlist.restrict_to_database_owner = on; SET ROLE mere_mortal; CREATE EXTENSION pg_stat_statements; ERROR: extension "pg_stat_statements" installation via pgextwlist is restricted to the database owner RESET ROLE; -- ===================================================================== -- 3. GUC on, DB owner (the pg_regress connection user, no SET ROLE): -- CREATE on whitelisted ext still works. -- ===================================================================== SET extwlist.restrict_to_database_owner = on; CREATE EXTENSION pg_stat_statements; DROP EXTENSION pg_stat_statements; -- (stat_resetters was already removed in test 1; the after-drop script -- recreated and re-dropped it implicitly.) -- ===================================================================== -- 4. GUC on, non-DB-owner, superuser-only extension: PG's superuser -- check fires; pgextwlist's GUC does not interfere. -- ===================================================================== SET extwlist.restrict_to_database_owner = on; SET ROLE mere_mortal; CREATE EXTENSION file_fdw; ERROR: permission denied to create extension "file_fdw" HINT: Must be superuser to create this extension. RESET ROLE; -- ===================================================================== -- 5. GUC on, non-DB-owner, trusted extension (btree_gin): -- PG's trusted-extension mechanism lets mere_mortal install it, -- provided mere_mortal has CREATE on the database. Grant + install + -- revoke + cleanup. -- ===================================================================== SET extwlist.restrict_to_database_owner = on; GRANT CREATE ON DATABASE contrib_regression TO mere_mortal; SET ROLE mere_mortal; CREATE EXTENSION btree_gin; ERROR: permission denied to create extension "btree_gin" HINT: Must be superuser to create this extension. RESET ROLE; DROP EXTENSION btree_gin; ERROR: extension "btree_gin" does not exist REVOKE CREATE ON DATABASE contrib_regression FROM mere_mortal; -- ===================================================================== -- 6. GUC on also blocks UPDATE / DROP / COMMENT for non-DB-owner. -- Install as DB owner first, then exercise the four operations as -- mere_mortal with GUC on. -- ===================================================================== SET extwlist.restrict_to_database_owner = off; CREATE EXTENSION pg_stat_statements; SET extwlist.restrict_to_database_owner = on; SET ROLE mere_mortal; ALTER EXTENSION pg_stat_statements UPDATE; ERROR: extension "pg_stat_statements" update via pgextwlist is restricted to the database owner DROP EXTENSION pg_stat_statements; ERROR: extension drop via pgextwlist is restricted to the database owner COMMENT ON EXTENSION pg_stat_statements IS 'probe'; ERROR: comment on extension "pg_stat_statements" via pgextwlist is restricted to the database owner RESET ROLE; -- Final cleanup. DROP EXTENSION IF EXISTS pg_stat_statements; DROP ROLE IF EXISTS stat_resetters; NOTICE: role "stat_resetters" does not exist, skipping RESET extwlist.restrict_to_database_owner; dimitri-pgextwlist-ab92a4c/expected/restrict_to_owner_1.out000066400000000000000000000104331522346476200244130ustar00rootroot00000000000000-- extwlist.restrict_to_database_owner -- -- When this GUC is on, pgextwlist only invokes its superuser-override path -- (CREATE / ALTER UPDATE / DROP / COMMENT EXTENSION on whitelisted -- extensions) when the current user is the database owner. Non-owners -- receive an explicit error. Trusted extensions and non-whitelisted -- extensions fall through to PG's normal checks. -- The pg_regress connection user owns contrib_regression in installcheck -- mode, so that user IS the database owner for these tests. -- Reset cluster-wide state that might be left over from prior runs. DROP EXTENSION IF EXISTS pg_stat_statements; NOTICE: extension "pg_stat_statements" does not exist, skipping DROP ROLE IF EXISTS stat_resetters; NOTICE: role "stat_resetters" does not exist, skipping DROP EXTENSION IF EXISTS btree_gin; NOTICE: extension "btree_gin" does not exist, skipping -- ===================================================================== -- 1. Default (GUC off) preserves current behavior. -- ===================================================================== SET ROLE mere_mortal; CREATE EXTENSION pg_stat_statements; RESET ROLE; DROP EXTENSION pg_stat_statements; DROP ROLE stat_resetters; -- ===================================================================== -- 2. GUC on, non-DB-owner (mere_mortal): CREATE on whitelisted ext is -- blocked by pgextwlist with an explicit error. -- ===================================================================== SET extwlist.restrict_to_database_owner = on; SET ROLE mere_mortal; CREATE EXTENSION pg_stat_statements; ERROR: extension "pg_stat_statements" installation via pgextwlist is restricted to the database owner RESET ROLE; -- ===================================================================== -- 3. GUC on, DB owner (the pg_regress connection user, no SET ROLE): -- CREATE on whitelisted ext still works. -- ===================================================================== SET extwlist.restrict_to_database_owner = on; CREATE EXTENSION pg_stat_statements; DROP EXTENSION pg_stat_statements; -- (stat_resetters was already removed in test 1; the after-drop script -- recreated and re-dropped it implicitly.) -- ===================================================================== -- 4. GUC on, non-DB-owner, superuser-only extension: PG's superuser -- check fires; pgextwlist's GUC does not interfere. -- ===================================================================== SET extwlist.restrict_to_database_owner = on; SET ROLE mere_mortal; CREATE EXTENSION file_fdw; ERROR: permission denied to create extension "file_fdw" HINT: Must be superuser to create this extension. RESET ROLE; -- ===================================================================== -- 5. GUC on, non-DB-owner, trusted extension (btree_gin): -- PG's trusted-extension mechanism lets mere_mortal install it, -- provided mere_mortal has CREATE on the database. Grant + install + -- revoke + cleanup. -- ===================================================================== SET extwlist.restrict_to_database_owner = on; GRANT CREATE ON DATABASE contrib_regression TO mere_mortal; SET ROLE mere_mortal; CREATE EXTENSION btree_gin; RESET ROLE; DROP EXTENSION btree_gin; REVOKE CREATE ON DATABASE contrib_regression FROM mere_mortal; -- ===================================================================== -- 6. GUC on also blocks UPDATE / DROP / COMMENT for non-DB-owner. -- Install as DB owner first, then exercise the four operations as -- mere_mortal with GUC on. -- ===================================================================== SET extwlist.restrict_to_database_owner = off; CREATE EXTENSION pg_stat_statements; SET extwlist.restrict_to_database_owner = on; SET ROLE mere_mortal; ALTER EXTENSION pg_stat_statements UPDATE; ERROR: extension "pg_stat_statements" update via pgextwlist is restricted to the database owner DROP EXTENSION pg_stat_statements; ERROR: extension drop via pgextwlist is restricted to the database owner COMMENT ON EXTENSION pg_stat_statements IS 'probe'; ERROR: comment on extension "pg_stat_statements" via pgextwlist is restricted to the database owner RESET ROLE; -- Final cleanup. DROP EXTENSION IF EXISTS pg_stat_statements; DROP ROLE IF EXISTS stat_resetters; NOTICE: role "stat_resetters" does not exist, skipping RESET extwlist.restrict_to_database_owner; dimitri-pgextwlist-ab92a4c/expected/setup.out000066400000000000000000000006701522346476200215620ustar00rootroot00000000000000LOAD 'pgextwlist'; ALTER SYSTEM SET session_preload_libraries='pgextwlist'; ALTER SYSTEM SET extwlist.extensions='citext,earthdistance,pg_trgm,pg_stat_statements,refint'; \set testdir `pwd` '/test-scripts' ALTER SYSTEM SET extwlist.custom_path=:'testdir'; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) DO LANGUAGE plpgsql $$ BEGIN CREATE ROLE mere_mortal; EXCEPTION WHEN duplicate_object THEN NULL; END $$; dimitri-pgextwlist-ab92a4c/expected/substitution_safety.out000066400000000000000000000045121522346476200245500ustar00rootroot00000000000000-- CVE-2023-39417 mitigation: @-substitution values containing characters -- that could enable SQL injection (" $ ' \) must be rejected before the -- replacement is inserted into the script. Verify each forbidden char. -- pg_stat_statements is whitelisted in setup.sql and has a custom after-create -- script under test-scripts/, so CREATE EXTENSION always reaches -- execute_custom_script and the substitution check. -- 1. Single quote in schema name. CREATE SCHEMA "weird'name"; SET ROLE mere_mortal; CREATE EXTENSION pg_stat_statements WITH SCHEMA "weird'name"; ERROR: invalid value for "@extschema@" substitution DETAIL: Value must not contain any of: "$'\ HINT: Rename the object referenced by @extschema@. RESET ROLE; DROP SCHEMA "weird'name"; -- 2. Dollar sign in schema name. CREATE SCHEMA "weird$name"; SET ROLE mere_mortal; CREATE EXTENSION pg_stat_statements WITH SCHEMA "weird$name"; ERROR: invalid value for "@extschema@" substitution DETAIL: Value must not contain any of: "$'\ HINT: Rename the object referenced by @extschema@. RESET ROLE; DROP SCHEMA "weird$name"; -- 3. Backslash in schema name. CREATE SCHEMA "weird\name"; SET ROLE mere_mortal; CREATE EXTENSION pg_stat_statements WITH SCHEMA "weird\name"; ERROR: invalid value for "@extschema@" substitution DETAIL: Value must not contain any of: "$'\ HINT: Rename the object referenced by @extschema@. RESET ROLE; DROP SCHEMA "weird\name"; -- 4. Double quote in schema name (embedded via SQL "" doubling). CREATE SCHEMA "weird""name"; SET ROLE mere_mortal; CREATE EXTENSION pg_stat_statements WITH SCHEMA "weird""name"; ERROR: invalid value for "@extschema@" substitution DETAIL: Value must not contain any of: "$'\ HINT: Rename the object referenced by @extschema@. RESET ROLE; DROP SCHEMA "weird""name"; -- Sanity: a plain schema name should not trip the CVE check. Clean up any -- leftovers from earlier regressions in the same cluster first so the -- after-create script can run cleanly. DROP EXTENSION IF EXISTS pg_stat_statements; NOTICE: extension "pg_stat_statements" does not exist, skipping DROP ROLE IF EXISTS stat_resetters; NOTICE: role "stat_resetters" does not exist, skipping CREATE SCHEMA ok_schema; SET ROLE mere_mortal; CREATE EXTENSION pg_stat_statements WITH SCHEMA ok_schema; RESET ROLE; DROP EXTENSION pg_stat_statements; DROP ROLE stat_resetters; DROP SCHEMA ok_schema; dimitri-pgextwlist-ab92a4c/pgextwlist.c000066400000000000000000000414521522346476200204510ustar00rootroot00000000000000/* * PostgreSQL Extension WhiteList -- Dimitri Fontaine * * Author: Dimitri Fontaine * Licence: PostgreSQL * Copyright Dimitri Fontaine, 2011-2013 * * For a description of the features see the README.md file from the same * distribution. */ #include #include #include #include #include "postgres.h" #include "pgextwlist.h" #include "utils.h" #include "access/genam.h" #include "access/heapam.h" #include "access/xact.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/objectaccess.h" #include "catalog/pg_auth_members.h" #include "catalog/pg_authid.h" #include "catalog/pg_database.h" #include "catalog/pg_db_role_setting.h" #include "catalog/namespace.h" #include "commands/comment.h" #include "commands/dbcommands.h" #include "commands/seclabel.h" #include "commands/user.h" #include "common/md5.h" #include "funcapi.h" #include "miscadmin.h" #include "storage/lmgr.h" #include "tcop/utility.h" #include "utils/acl.h" #include "utils/builtins.h" #include "utils/fmgroids.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/timestamp.h" #if PG_MAJOR_VERSION < 1200 #include "utils/tqual.h" #endif #include "utils/varlena.h" /* * This code has only been tested with PostgreSQL 9.1. * * It should be "deprecated" in 9.2 and following thanks to command triggers, * and the extension mechanism it works with didn't exist before 9.1. */ PG_MODULE_MAGIC; char *extwlist_extensions = NULL; char *extwlist_custom_path = NULL; bool extwlist_restrict_to_database_owner = false; static ProcessUtility_hook_type prev_ProcessUtility = NULL; void _PG_init(void); void _PG_fini(void); #if PG_MAJOR_VERSION < 1300 #define PROCESS_UTILITY_PROTO_ARGS PlannedStmt *pstmt, \ const char *queryString, \ ProcessUtilityContext context, \ ParamListInfo params, \ QueryEnvironment *queryEnv, \ DestReceiver *dest, \ char *completionTag #define PROCESS_UTILITY_ARGS pstmt, queryString, context, \ params, queryEnv, dest, completionTag #elif PG_MAJOR_VERSION < 1400 #define PROCESS_UTILITY_PROTO_ARGS PlannedStmt *pstmt, \ const char *queryString, \ ProcessUtilityContext context, \ ParamListInfo params, \ QueryEnvironment *queryEnv, \ DestReceiver *dest, \ QueryCompletion *qc #define PROCESS_UTILITY_ARGS pstmt, queryString, context, \ params, queryEnv, dest, qc #else #define PROCESS_UTILITY_PROTO_ARGS PlannedStmt *pstmt, \ const char *queryString, \ bool readOnlyTree, \ ProcessUtilityContext context, \ ParamListInfo params, \ QueryEnvironment *queryEnv, \ DestReceiver *dest, \ QueryCompletion *qc #define PROCESS_UTILITY_ARGS pstmt, queryString, readOnlyTree, context, \ params, queryEnv, dest, qc #endif /* PG_MAJOR_VERSION */ #define EREPORT_EXTENSION_IS_NOT_WHITELISTED(op) \ ereport(ERROR, \ (errcode(ERRCODE_INVALID_PARAMETER_VALUE), \ errmsg("extension \"%s\" is not whitelisted", name), \ errdetail("%s the extension \"%s\" failed, " \ "because it is not on the whitelist of " \ "user-installable extensions.", op, name), \ errhint("Your system administrator has allowed users " \ "to install certain extensions. " \ "See: SHOW extwlist.extensions;"))); static void extwlist_ProcessUtility(PROCESS_UTILITY_PROTO_ARGS); static void call_ProcessUtility(PROCESS_UTILITY_PROTO_ARGS, const char *name, const char *schema, const char *old_version, const char *new_version, const char *action); static void call_RawProcessUtility(PROCESS_UTILITY_PROTO_ARGS); /* * Check that the configured extwlist.custom_path exists, is a directory, * and is reachable. Empty/NULL disables the feature (no validation). * * Failing here is intentional: a misconfigured custom_path would silently * bypass intended extension hooks, which defeats the security purpose of * the whitelist. Better to refuse the configuration outright. */ static void check_extwlist_custom_path_impl(const char *newval) { struct stat st; if (newval == NULL || newval[0] == '\0') return; if (stat(newval, &st) != 0) ereport(ERROR, (errcode_for_file_access(), errmsg("invalid value for parameter \"extwlist.custom_path\": " "could not access \"%s\": %m", newval))); if (!S_ISDIR(st.st_mode)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid value for parameter \"extwlist.custom_path\": " "\"%s\" is not a directory", newval))); if (access(newval, R_OK | X_OK) != 0) ereport(ERROR, (errcode_for_file_access(), errmsg("invalid value for parameter \"extwlist.custom_path\": " "directory \"%s\" is not readable: %m", newval))); } static bool check_extwlist_custom_path(char **newval, void **extra, GucSource source) { check_extwlist_custom_path_impl(*newval); return true; } /* * _PG_init() - library load-time initialization * * DO NOT make this static nor change its name! * * Init the module, all we have to do here is getting our GUC */ void _PG_init(void) { DefineCustomStringVariable("extwlist.extensions", "List of extensions that are whitelisted", "Separated by comma", &extwlist_extensions, "", PGC_SUSET, GUC_NOT_IN_SAMPLE, NULL, NULL, NULL); DefineCustomStringVariable("extwlist.custom_path", "Directory where to load custom scripts from", "Must point to an existing, readable directory; " "empty disables the custom-script feature.", &extwlist_custom_path, "", PGC_SUSET, GUC_NOT_IN_SAMPLE, check_extwlist_custom_path, NULL, NULL); DefineCustomBoolVariable("extwlist.restrict_to_database_owner", "Restrict pgextwlist superuser override to the database owner", "When on, only the database owner may use pgextwlist " "to install, update, drop, or comment on whitelisted " "extensions. Trusted extensions are unaffected.", &extwlist_restrict_to_database_owner, false, PGC_SUSET, GUC_NOT_IN_SAMPLE, NULL, NULL, NULL); EmitWarningsOnPlaceholders("extwlist"); prev_ProcessUtility = ProcessUtility_hook; ProcessUtility_hook = extwlist_ProcessUtility; } /* * Extension Whitelisting includes mechanisms to run custom scripts before and * after the extension's provided script. * * We lookup scripts at the following places and run them when they exist: * * ${extwlist_custom_path}/${extname}/${when}--${version}.sql (create) * ${extwlist_custom_path}/${extname}/${when}--${oldversion}--${newversion}.sql (upgrade) * ${extwlist_custom_path}/${extname}/${when}-${action}--${version}.sql (rest) * ${extwlist_custom_path}/${extname}/${when}-${action}.sql (all actions) * * - action is expected to be one of "create", "update", "comment", or "drop" * - when is expected to be either "before" or "after" * * We don't validation the extension's name before building the scripts path * here because the extension name we are dealing with must have already been * added to the whitelist, which should be enough of a validation step. */ static void call_extension_scripts(const char *extname, const char *schema, const char *action, const char *when, const char *from_version, const char *version) { char *specific_custom_script; char *generic_custom_script; /* Nothing to do if the custom-script feature is disabled. */ if (extwlist_custom_path == NULL || extwlist_custom_path[0] == '\0') return; /* * Ensure the per-extension subdirectory exists and is reachable. ENOENT * is treated as "no custom scripts for this extension" and silently * skipped; anything else is a hard error to avoid bypassing the whitelist * via filesystem misconfiguration. */ validate_custom_script_dir(extname); if (version) { specific_custom_script = get_specific_custom_script_filename(extname, when, from_version, version); elog(DEBUG1, "Considering custom script \"%s\"", specific_custom_script); if (access(specific_custom_script, F_OK) == 0) { execute_custom_script(specific_custom_script, schema); return; /* skip generic script */ } } generic_custom_script = get_generic_custom_script_filename(extname, action, when); elog(DEBUG1, "Considering custom script \"%s\"", generic_custom_script); if (access(generic_custom_script, F_OK) == 0) execute_custom_script(generic_custom_script, schema); } static bool extension_is_whitelisted(const char *name) { bool whitelisted = false; char *rawnames = pstrdup(extwlist_extensions); List *extensions; ListCell *lc; if (!SplitIdentifierString(rawnames, ',', &extensions)) { /* syntax error in extension name list */ ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("parameter \"extwlist.extensions\" must be a list of extension names"))); } foreach(lc, extensions) { char *curext = (char *) lfirst(lc); if (strcmp(name, curext) == 0) { whitelisted = true; break; } } return whitelisted; } /* * Return true if the current user owns the current database. Used to gate * pgextwlist's superuser-override path when extwlist.restrict_to_database_owner * is enabled. PG 16 removed pg_database_ownercheck() in favour of the generic * object_ownercheck(), so we dispatch on version. */ static bool current_user_is_database_owner(void) { #if PG_MAJOR_VERSION >= 1600 return object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId()); #else return pg_database_ownercheck(MyDatabaseId, GetUserId()); #endif } /* * ProcessUtility hook */ static void extwlist_ProcessUtility(PROCESS_UTILITY_PROTO_ARGS) { char *name = NULL; char *schema = NULL; char *old_version = NULL; char *new_version = NULL; Node *parsetree = pstmt->utilityStmt; /* * Don't try to make life hard for our friendly superusers. Also, if a * valid transaction is not ongoing then return early. */ if (!IsTransactionState() || superuser()) { call_RawProcessUtility(PROCESS_UTILITY_ARGS); return; } switch (nodeTag(parsetree)) { case T_CreateExtensionStmt: { CreateExtensionStmt *stmt = (CreateExtensionStmt *) parsetree; name = stmt->extname; fill_in_extension_properties(name, stmt->options, &schema, &old_version, &new_version); if (extension_is_whitelisted(name)) { if (extwlist_restrict_to_database_owner && !current_user_is_database_owner()) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("extension \"%s\" installation via pgextwlist " "is restricted to the database owner", name))); call_ProcessUtility(PROCESS_UTILITY_ARGS, name, schema, old_version, new_version, "create"); return; } break; } case T_AlterExtensionStmt: { AlterExtensionStmt *stmt = (AlterExtensionStmt *) parsetree; name = stmt->extname; fill_in_extension_properties(name, stmt->options, &schema, &old_version, &new_version); /* fetch old_version from the catalogs, actually */ old_version = get_extension_current_version(name); if (extension_is_whitelisted(name)) { if (extwlist_restrict_to_database_owner && !current_user_is_database_owner()) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("extension \"%s\" update via pgextwlist " "is restricted to the database owner", name))); call_ProcessUtility(PROCESS_UTILITY_ARGS, name, schema, old_version, new_version, "update"); return; } break; } case T_DropStmt: if (((DropStmt *) parsetree)->removeType == OBJECT_EXTENSION) { /* DROP EXTENSION can target several of them at once */ bool all_in_whitelist = true; ListCell *lc; foreach(lc, ((DropStmt *) parsetree)->objects) { /* * For deconstructing the object list into actual names, * see the get_object_address_unqualified() function in * src/backend/catalog/objectaddress.c */ bool whitelisted = false; List *objname = lfirst(lc); #if PG_MAJOR_VERSION < 1500 name = strVal((Value *) objname); #else name = strVal(castNode(String, objname)); #endif whitelisted = extension_is_whitelisted(name); all_in_whitelist = all_in_whitelist && whitelisted; } /* * If we have a mix of whitelisted and non-whitelisted * extensions in a single DROP EXTENSION command, better play * safe and do the DROP without superpowers. * * So we only give superpowers when all extensions are in the * whitelist. */ if (all_in_whitelist) { if (extwlist_restrict_to_database_owner && !current_user_is_database_owner()) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("extension drop via pgextwlist " "is restricted to the database owner"))); call_ProcessUtility(PROCESS_UTILITY_ARGS, NULL, "", /* schema must not be NULL */ NULL, NULL, "drop"); return; } } break; case T_CommentStmt: { CommentStmt *stmt = (CommentStmt *) parsetree; if (stmt->objtype == OBJECT_EXTENSION) { #if PG_MAJOR_VERSION < 1500 name = strVal((Value *) stmt->object); #else name = strVal(castNode(String, stmt->object)); #endif if (extension_is_whitelisted(name)) { if (extwlist_restrict_to_database_owner && !current_user_is_database_owner()) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("comment on extension \"%s\" via pgextwlist " "is restricted to the database owner", name))); call_ProcessUtility(PROCESS_UTILITY_ARGS, name, "", /* schema must not be * NULL */ NULL, NULL, "comment"); return; } } break; } /* We intentionally don't support that command. */ case T_AlterExtensionContentsStmt: default: break; } /* * We can only fall here if we don't want to support the command, so pass * control over to the usual processing. */ call_RawProcessUtility(PROCESS_UTILITY_ARGS); } /* * Change current user and security context as if running a SECURITY DEFINER * procedure owned by a superuser, hard coded as the bootstrap user. */ static void call_ProcessUtility(PROCESS_UTILITY_PROTO_ARGS, const char *name, const char *schema, const char *old_version, const char *new_version, const char *action) { Oid save_userid; int save_sec_context; GetUserIdAndSecContext(&save_userid, &save_sec_context); SetUserIdAndSecContext(BOOTSTRAP_SUPERUSERID, save_sec_context | SECURITY_LOCAL_USERID_CHANGE | SECURITY_RESTRICTED_OPERATION); if (action) { /* "drop extension" can list several extensions, walk them here */ if (strcmp(action, "drop") == 0) { Node *parsetree = pstmt->utilityStmt; ListCell *lc; char *name = NULL; foreach(lc, ((DropStmt *) parsetree)->objects) { List *objname = lfirst(lc); #if PG_MAJOR_VERSION < 1500 name = strVal((Value *) objname); #else name = strVal(castNode(String, objname)); #endif call_extension_scripts(name, schema, action, "before", old_version, new_version); } } else call_extension_scripts(name, schema, action, "before", old_version, new_version); } call_RawProcessUtility(PROCESS_UTILITY_ARGS); if (action) { if (strcmp(action, "drop") == 0) { Node *parsetree = pstmt->utilityStmt; ListCell *lc; char *name = NULL; foreach(lc, ((DropStmt *) parsetree)->objects) { List *objname = lfirst(lc); #if PG_MAJOR_VERSION < 1500 name = strVal((Value *) objname); #else name = strVal(castNode(String, objname)); #endif call_extension_scripts(name, schema, action, "after", old_version, new_version); } } else call_extension_scripts(name, schema, action, "after", old_version, new_version); } SetUserIdAndSecContext(save_userid, save_sec_context); } static void call_RawProcessUtility(PROCESS_UTILITY_PROTO_ARGS) { if (prev_ProcessUtility) prev_ProcessUtility(PROCESS_UTILITY_ARGS); else standard_ProcessUtility(PROCESS_UTILITY_ARGS); } dimitri-pgextwlist-ab92a4c/pgextwlist.h000066400000000000000000000007271522346476200204560ustar00rootroot00000000000000/* * PostgreSQL Extension WhiteList -- Dimitri Fontaine * * Author: Dimitri Fontaine * Licence: PostgreSQL * Copyright Dimitri Fontaine, 2011-2013 * * For a description of the features see the README.md file from the same * distribution. */ #ifdef PG_VERSION_NUM #define PG_MAJOR_VERSION (PG_VERSION_NUM / 100) #else #error "Unknown PostgreSQL version" #endif #if PG_MAJOR_VERSION < 1000 #error "Unsupported postgresql version" #endif dimitri-pgextwlist-ab92a4c/pgextwlist.spec000066400000000000000000000025721522346476200211610ustar00rootroot00000000000000Name: %{?package_prefix}pgextwlist Version: %{major_version} Release: %{minor_version}%{?dist} Summary: PostgreSQL Extension Whitelist extension Group: Applications/Databases License: PostgreSQL Requires: %{?package_prefix}%{!?package_prefix:postgresql-}server BuildRequires: %{?package_prefix}%{!?package_prefix:postgresql-}devel Source0: pgextwlist-rpm-src.tar.gz %description This extension implements extension whitelisting, and will actively prevent users from installing extensions not in the provided list. Also, this extension implements a form of sudo facility in that the whitelisted extensions will get installed as if superuser. Privileges are dropped before handing the control back to the user. %prep %setup -q -n pgextwlist %build sed '/^DOCS/d' -i Makefile # don't install README.md in pgsql/contrib make %install rm -rf %{buildroot} make install DESTDIR=%{buildroot} %clean rm -rf %{buildroot} %files %defattr(-,root,root,-) %doc README.md %{?pkglibdir}%{!?pkglibdir:%{_libdir}/pgsql}/ %changelog * Sun Oct 15 2017 Christoph Berg - 1.6-0 - New upstream version. * Tue Sep 27 2016 Christoph Berg - 1.5-0 - New upstream version. * Tue Feb 02 2016 Christoph Berg - 1.4-0 - New upstream version. * Tue Jul 14 2015 Oskari Saarenmaa - 1.3-0 - Initial. dimitri-pgextwlist-ab92a4c/sql/000077500000000000000000000000001522346476200166645ustar00rootroot00000000000000dimitri-pgextwlist-ab92a4c/sql/crossuser.sql000066400000000000000000000004461522346476200214410ustar00rootroot00000000000000GRANT USAGE, CREATE ON SCHEMA public TO public; DO LANGUAGE plpgsql $$ BEGIN CREATE ROLE evil_user; EXCEPTION WHEN duplicate_object THEN NULL; END $$; SET ROLE mere_mortal; CREATE TABLE mere_table (t citext); SET ROLE evil_user; DROP EXTENSION citext; DROP EXTENSION citext CASCADE; dimitri-pgextwlist-ab92a4c/sql/errors.sql000066400000000000000000000014371522346476200207260ustar00rootroot00000000000000SELECT case when setting::int >= 130000 then 'PG 13+' when setting::int >= 100000 then 'PG 10..12' end as "regression output for PG version" FROM pg_settings where name = 'server_version_num'; SET ROLE mere_mortal; SELECT extname FROM pg_extension ORDER BY 1; -- pre-existing extension CREATE EXTENSION plpgsql; COMMENT ON EXTENSION hstore IS 'plpgsql comment'; SELECT extname FROM pg_extension ORDER BY 1; -- non-whitelisted extension CREATE EXTENSION hstore; COMMENT ON EXTENSION hstore IS 'hstore comment'; SELECT extname FROM pg_extension ORDER BY 1; -- whitelisted extension, but dependency is missing CREATE EXTENSION earthdistance; SELECT extname FROM pg_extension ORDER BY 1; -- drop non-whitelisted extension DROP EXTENSION plpgsql; SELECT extname FROM pg_extension ORDER BY 1; dimitri-pgextwlist-ab92a4c/sql/hooks.sql000066400000000000000000000006601522346476200205320ustar00rootroot00000000000000select case when setting::int >= 160000 then 'PG 16+' end as "regression output for PG version" from pg_settings where name = 'server_version_num'; set client_min_messages = debug; set extwlist.custom_path = ''; set role mere_mortal; -- in the hope refint stays at version 1.0 in PostgreSQL create extension refint; alter extension refint update; comment on extension refint is 'snarky remark'; drop extension refint, refint; dimitri-pgextwlist-ab92a4c/sql/path_validation.sql000066400000000000000000000033361522346476200225600ustar00rootroot00000000000000-- extwlist.custom_path validation -- -- The configured path must exist and be a readable directory; an -- unreadable or missing top-level path is a hard ERROR to avoid silently -- bypassing the extension whitelist via filesystem misconfiguration. \set goodpath `pwd` '/test-scripts' \set regfile '/etc/passwd' -- 1. Nonexistent path is rejected at SET time. SET extwlist.custom_path = '/no/such/directory/pgextwlist-test'; -- Value should be unchanged after rejection (still the cluster default). SELECT substring(setting FROM '/([^/]*)$') AS suffix FROM pg_settings WHERE name = 'extwlist.custom_path'; -- 2. A regular file is rejected (not a directory). SET extwlist.custom_path = :'regfile'; SELECT substring(setting FROM '/([^/]*)$') AS suffix FROM pg_settings WHERE name = 'extwlist.custom_path'; -- 3. Empty string disables the feature (no validation, no script lookup). SET extwlist.custom_path = ''; SELECT setting AS suffix FROM pg_settings WHERE name = 'extwlist.custom_path'; -- With feature off, CREATE EXTENSION on a whitelisted extension succeeds -- even though no custom_path is configured. SET ROLE mere_mortal; CREATE EXTENSION citext; DROP EXTENSION citext; RESET ROLE; -- 4. Setting back to a valid directory is accepted. SET extwlist.custom_path = :'goodpath'; SELECT substring(setting FROM '/([^/]*)$') AS suffix FROM pg_settings WHERE name = 'extwlist.custom_path'; -- Per-extension subdirectory absent is a silent skip: CREATE EXTENSION -- on a whitelisted extension whose subdir does not exist under the -- custom_path must succeed without error. SET ROLE mere_mortal; CREATE EXTENSION refint; DROP EXTENSION refint; RESET ROLE; -- 5. Restore default so downstream tests start from a known state. RESET extwlist.custom_path; dimitri-pgextwlist-ab92a4c/sql/pgextwlist.sql000066400000000000000000000015111522346476200216150ustar00rootroot00000000000000SET ROLE mere_mortal; SHOW extwlist.extensions; SELECT extname FROM pg_extension ORDER BY 1; -- whitelisted extensions CREATE EXTENSION citext; CREATE EXTENSION pg_trgm; COMMENT ON EXTENSION pg_trgm IS 'pg_trgm comment'; SELECT extname FROM pg_extension ORDER BY 1; SELECT d.description FROM pg_extension e JOIN pg_description d ON d.objoid = e.oid WHERE e.extname = 'pg_trgm'; -- drop whitelisted extension DROP EXTENSION pg_trgm; SELECT extname FROM pg_extension ORDER BY 1; -- whitelisted extension with custom scripts CREATE EXTENSION pg_stat_statements; SELECT extname FROM pg_extension ORDER BY 1; SELECT proacl FROM pg_proc WHERE proname = 'pg_stat_statements_reset'; SELECT groname FROM pg_group WHERE groname = 'stat_resetters'; DROP EXTENSION pg_stat_statements; SELECT groname FROM pg_group WHERE groname = 'stat_resetters'; dimitri-pgextwlist-ab92a4c/sql/restrict_to_owner.sql000066400000000000000000000071301522346476200231610ustar00rootroot00000000000000-- extwlist.restrict_to_database_owner -- -- When this GUC is on, pgextwlist only invokes its superuser-override path -- (CREATE / ALTER UPDATE / DROP / COMMENT EXTENSION on whitelisted -- extensions) when the current user is the database owner. Non-owners -- receive an explicit error. Trusted extensions and non-whitelisted -- extensions fall through to PG's normal checks. -- The pg_regress connection user owns contrib_regression in installcheck -- mode, so that user IS the database owner for these tests. -- Reset cluster-wide state that might be left over from prior runs. DROP EXTENSION IF EXISTS pg_stat_statements; DROP ROLE IF EXISTS stat_resetters; DROP EXTENSION IF EXISTS btree_gin; -- ===================================================================== -- 1. Default (GUC off) preserves current behavior. -- ===================================================================== SET ROLE mere_mortal; CREATE EXTENSION pg_stat_statements; RESET ROLE; DROP EXTENSION pg_stat_statements; DROP ROLE stat_resetters; -- ===================================================================== -- 2. GUC on, non-DB-owner (mere_mortal): CREATE on whitelisted ext is -- blocked by pgextwlist with an explicit error. -- ===================================================================== SET extwlist.restrict_to_database_owner = on; SET ROLE mere_mortal; CREATE EXTENSION pg_stat_statements; RESET ROLE; -- ===================================================================== -- 3. GUC on, DB owner (the pg_regress connection user, no SET ROLE): -- CREATE on whitelisted ext still works. -- ===================================================================== SET extwlist.restrict_to_database_owner = on; CREATE EXTENSION pg_stat_statements; DROP EXTENSION pg_stat_statements; -- (stat_resetters was already removed in test 1; the after-drop script -- recreated and re-dropped it implicitly.) -- ===================================================================== -- 4. GUC on, non-DB-owner, superuser-only extension: PG's superuser -- check fires; pgextwlist's GUC does not interfere. -- ===================================================================== SET extwlist.restrict_to_database_owner = on; SET ROLE mere_mortal; CREATE EXTENSION file_fdw; RESET ROLE; -- ===================================================================== -- 5. GUC on, non-DB-owner, trusted extension (btree_gin): -- PG's trusted-extension mechanism lets mere_mortal install it, -- provided mere_mortal has CREATE on the database. Grant + install + -- revoke + cleanup. -- ===================================================================== SET extwlist.restrict_to_database_owner = on; GRANT CREATE ON DATABASE contrib_regression TO mere_mortal; SET ROLE mere_mortal; CREATE EXTENSION btree_gin; RESET ROLE; DROP EXTENSION btree_gin; REVOKE CREATE ON DATABASE contrib_regression FROM mere_mortal; -- ===================================================================== -- 6. GUC on also blocks UPDATE / DROP / COMMENT for non-DB-owner. -- Install as DB owner first, then exercise the four operations as -- mere_mortal with GUC on. -- ===================================================================== SET extwlist.restrict_to_database_owner = off; CREATE EXTENSION pg_stat_statements; SET extwlist.restrict_to_database_owner = on; SET ROLE mere_mortal; ALTER EXTENSION pg_stat_statements UPDATE; DROP EXTENSION pg_stat_statements; COMMENT ON EXTENSION pg_stat_statements IS 'probe'; RESET ROLE; -- Final cleanup. DROP EXTENSION IF EXISTS pg_stat_statements; DROP ROLE IF EXISTS stat_resetters; RESET extwlist.restrict_to_database_owner; dimitri-pgextwlist-ab92a4c/sql/setup.sql000066400000000000000000000006131522346476200205450ustar00rootroot00000000000000LOAD 'pgextwlist'; ALTER SYSTEM SET session_preload_libraries='pgextwlist'; ALTER SYSTEM SET extwlist.extensions='citext,earthdistance,pg_trgm,pg_stat_statements,refint'; \set testdir `pwd` '/test-scripts' ALTER SYSTEM SET extwlist.custom_path=:'testdir'; SELECT pg_reload_conf(); DO LANGUAGE plpgsql $$ BEGIN CREATE ROLE mere_mortal; EXCEPTION WHEN duplicate_object THEN NULL; END $$; dimitri-pgextwlist-ab92a4c/sql/substitution_safety.sql000066400000000000000000000031771522346476200235440ustar00rootroot00000000000000-- CVE-2023-39417 mitigation: @-substitution values containing characters -- that could enable SQL injection (" $ ' \) must be rejected before the -- replacement is inserted into the script. Verify each forbidden char. -- pg_stat_statements is whitelisted in setup.sql and has a custom after-create -- script under test-scripts/, so CREATE EXTENSION always reaches -- execute_custom_script and the substitution check. -- 1. Single quote in schema name. CREATE SCHEMA "weird'name"; SET ROLE mere_mortal; CREATE EXTENSION pg_stat_statements WITH SCHEMA "weird'name"; RESET ROLE; DROP SCHEMA "weird'name"; -- 2. Dollar sign in schema name. CREATE SCHEMA "weird$name"; SET ROLE mere_mortal; CREATE EXTENSION pg_stat_statements WITH SCHEMA "weird$name"; RESET ROLE; DROP SCHEMA "weird$name"; -- 3. Backslash in schema name. CREATE SCHEMA "weird\name"; SET ROLE mere_mortal; CREATE EXTENSION pg_stat_statements WITH SCHEMA "weird\name"; RESET ROLE; DROP SCHEMA "weird\name"; -- 4. Double quote in schema name (embedded via SQL "" doubling). CREATE SCHEMA "weird""name"; SET ROLE mere_mortal; CREATE EXTENSION pg_stat_statements WITH SCHEMA "weird""name"; RESET ROLE; DROP SCHEMA "weird""name"; -- Sanity: a plain schema name should not trip the CVE check. Clean up any -- leftovers from earlier regressions in the same cluster first so the -- after-create script can run cleanly. DROP EXTENSION IF EXISTS pg_stat_statements; DROP ROLE IF EXISTS stat_resetters; CREATE SCHEMA ok_schema; SET ROLE mere_mortal; CREATE EXTENSION pg_stat_statements WITH SCHEMA ok_schema; RESET ROLE; DROP EXTENSION pg_stat_statements; DROP ROLE stat_resetters; DROP SCHEMA ok_schema; dimitri-pgextwlist-ab92a4c/test-scripts/000077500000000000000000000000001522346476200205315ustar00rootroot00000000000000dimitri-pgextwlist-ab92a4c/test-scripts/pg_stat_statements/000077500000000000000000000000001522346476200244415ustar00rootroot00000000000000dimitri-pgextwlist-ab92a4c/test-scripts/pg_stat_statements/after-create.sql000066400000000000000000000002401522346476200275200ustar00rootroot00000000000000ALTER FUNCTION pg_stat_statements_reset OWNER TO mere_mortal; CREATE ROLE stat_resetters; GRANT EXECUTE ON FUNCTION pg_stat_statements_reset TO stat_resetters; dimitri-pgextwlist-ab92a4c/test-scripts/pg_stat_statements/after-drop.sql000066400000000000000000000000321522346476200272200ustar00rootroot00000000000000DROP ROLE stat_resetters; dimitri-pgextwlist-ab92a4c/utils.c000066400000000000000000000461751522346476200174060ustar00rootroot00000000000000/* * PostgreSQL Extension WhiteList -- Dimitri Fontaine * * Author: Dimitri Fontaine * Licence: PostgreSQL * Copyright Dimitri Fontaine, 2011-2013 * * For a description of the features see the README.md file from the same * distribution. */ /* * Some tools to read a SQL file and execute commands found in there. * * The following code comes from the PostgreSQL source tree in * src/backend/commands/extension.c, with some modifications to run in the * context of the Extension Whitelisting Extension. */ #include #include #include #include "postgres.h" #include "pgextwlist.h" #include "utils.h" #include "access/htup_details.h" #include "access/genam.h" #include "access/heapam.h" #include "access/skey.h" #include "access/xact.h" #include "catalog/indexing.h" #include "catalog/namespace.h" #include "catalog/pg_collation.h" #include "catalog/pg_database.h" #include "catalog/pg_extension.h" #include "commands/extension.h" #include "executor/executor.h" #include "mb/pg_wchar.h" #include "miscadmin.h" #include "storage/fd.h" #include "tcop/pquery.h" #include "tcop/utility.h" #include "utils/builtins.h" #include "utils/memutils.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" #include "utils/snapmgr.h" #include "utils/syscache.h" #if PG_MAJOR_VERSION < 1200 #include "utils/tqual.h" #define table_open(r, l) heap_open(r, l) #define table_close(r, l) heap_close(r, l) #endif /* * Parse contents of primary or auxiliary control file, and fill in * fields of *control. We parse primary file if version == NULL, * else the optional auxiliary file for that version. * * Control files are supposed to be very short, half a dozen lines, * so we don't worry about memory allocation risks here. Also we don't * worry about what encoding it's in; all values are expected to be ASCII. */ static void parse_default_version_in_control_file(const char *extname, char **version, char **schema) { char sharepath[MAXPGPATH]; char *filename; FILE *file; ConfigVariable *item, *head = NULL, *tail = NULL; /* * Locate the file to read. */ get_share_path(my_exec_path, sharepath); filename = (char *) palloc(MAXPGPATH); snprintf(filename, MAXPGPATH, "%s/extension/%s.control", sharepath, extname); if ((file = AllocateFile(filename, "r")) == NULL) { /* we still need to handle the following error here */ ereport(ERROR, (errcode_for_file_access(), errmsg("could not open extension control file \"%s\": %m", filename))); } /* * Parse the file content, using GUC's file parsing code. We need not * check the return value since any errors will be thrown at ERROR level. */ (void) ParseConfigFp(file, filename, 0, ERROR, &head, &tail); FreeFile(file); /* * Convert the ConfigVariable list into ExtensionControlFile entries, we * are only interested into the default version. */ for (item = head; item != NULL; item = item->next) { if (*version == NULL && strcmp(item->name, "default_version") == 0) { *version = pstrdup(item->value); } else if (*schema == NULL && strcmp(item->name, "schema") == 0) { *schema = pstrdup(item->value); } } FreeConfigVariables(head); pfree(filename); } /* * Returns true when extwlist.custom_path is set to a usable value, false * when it's NULL or empty (feature disabled). */ static bool custom_path_is_set(void) { return extwlist_custom_path != NULL && extwlist_custom_path[0] != '\0'; } /* * We lookup scripts at the following places and run them when they exist: * * ${extwlist_custom_path}/${extname}/${when}--${version}.sql (upgrade) * ${extwlist_custom_path}/${extname}/${when}-${action}.sql (create) * ${extwlist_custom_path}/${extname}/${when}-${action}--${version}.sql (rest) */ char * get_generic_custom_script_filename(const char *name, const char *action, const char *when) { char *result; if (!custom_path_is_set()) return NULL; result = (char *) palloc(MAXPGPATH); snprintf(result, MAXPGPATH, "%s/%s/%s-%s.sql", extwlist_custom_path, name, when, action); return result; } char * get_specific_custom_script_filename(const char *name, const char *when, const char *from_version, const char *version) { char *result; if (!custom_path_is_set()) return NULL; result = (char *) palloc(MAXPGPATH); if (from_version) snprintf(result, MAXPGPATH, "%s/%s/%s--%s--%s.sql", extwlist_custom_path, name, when, from_version, version); else snprintf(result, MAXPGPATH, "%s/%s/%s--%s.sql", extwlist_custom_path, name, when, version); return result; } /* * Build ${extwlist.custom_path}/${extname} into a palloc'd buffer. Returns * NULL if custom_path is unset; the caller is responsible for pfree. */ char * get_custom_script_dir(const char *extname) { char *result; if (!custom_path_is_set()) return NULL; result = (char *) palloc(MAXPGPATH); snprintf(result, MAXPGPATH, "%s/%s", extwlist_custom_path, extname); return result; } /* * Ensure the per-extension subdirectory under extwlist.custom_path is * reachable. ENOENT means the extension simply has no custom scripts and * is treated as a silent skip; any other stat() failure is a hard error * because silently dropping hooks would defeat the security purpose of * the whitelist. */ void validate_custom_script_dir(const char *extname) { char *dir; struct stat st; if (!custom_path_is_set()) return; dir = get_custom_script_dir(extname); if (stat(dir, &st) != 0) { if (errno == ENOENT) { /* subdir legitimately absent; nothing to run */ pfree(dir); return; } ereport(ERROR, (errcode_for_file_access(), errmsg("could not access custom script directory \"%s\": %m", dir))); } if (!S_ISDIR(st.st_mode)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("custom script path \"%s\" is not a directory", dir))); pfree(dir); } /* * At CREATE EXTENSION UPDATE time, we generally aren't provided with the * current version of the extension to upgrade, go fetch it from the catalogs. */ char * get_extension_current_version(const char *extname) { char *oldVersionName; Relation extRel; ScanKeyData key[1]; SysScanDesc extScan; HeapTuple extTup; Datum datum; bool isnull; /* * Look up the extension --- it must already exist in pg_extension */ extRel = table_open(ExtensionRelationId, AccessShareLock); ScanKeyInit(&key[0], Anum_pg_extension_extname, BTEqualStrategyNumber, F_NAMEEQ, CStringGetDatum(extname)); extScan = systable_beginscan(extRel, ExtensionNameIndexId, true, SnapshotSelf, 1, key); extTup = systable_getnext(extScan); if (!HeapTupleIsValid(extTup)) ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT), errmsg("extension \"%s\" does not exist", extname))); /* * Determine the existing version we are updating from */ datum = heap_getattr(extTup, Anum_pg_extension_extversion, RelationGetDescr(extRel), &isnull); if (isnull) elog(ERROR, "extversion is null"); oldVersionName = text_to_cstring(DatumGetTextPP(datum)); systable_endscan(extScan); table_close(extRel, AccessShareLock); return oldVersionName; } /* * Read the statement's option list and set given parameters. */ void fill_in_extension_properties(const char *extname, List *options, char **schema, char **old_version, char **new_version) { ListCell *lc; DefElem *d_schema = NULL; DefElem *d_new_version = NULL; DefElem *d_old_version = NULL; /* * Read the statement option list, taking care not to issue any errors * here ourselves if at all possible: let the core code handle them. */ foreach(lc, options) { DefElem *defel = (DefElem *) lfirst(lc); if (strcmp(defel->defname, "schema") == 0) { d_schema = defel; } else if (strcmp(defel->defname, "new_version") == 0) { d_new_version = defel; } else if (strcmp(defel->defname, "old_version") == 0) { d_old_version = defel; } else { /* intentionally don't try and catch errors here */ } } if (d_schema && d_schema->arg) *schema = strVal(d_schema->arg); if (d_old_version && d_old_version->arg) *old_version = strVal(d_old_version->arg); if (d_new_version && d_new_version->arg) *new_version = strVal(d_new_version->arg); if (*new_version == NULL || *schema == NULL) /* fetch the default_version from the extension's control file */ parse_default_version_in_control_file(extname, new_version, schema); /* schema might be given neither in the statement nor the control file */ if (*schema == NULL) { /* * Use the current default creation namespace, which is the first * explicit entry in the search_path. */ Oid schemaOid; List *search_path = fetch_search_path(false); if (search_path == NIL) /* nothing valid in search_path? */ ereport(ERROR, (errcode(ERRCODE_UNDEFINED_SCHEMA), errmsg("no schema has been selected to create in"))); schemaOid = linitial_oid(search_path); *schema = get_namespace_name(schemaOid); if (*schema == NULL) /* recently-deleted namespace? */ ereport(ERROR, (errcode(ERRCODE_UNDEFINED_SCHEMA), errmsg("no schema has been selected to create in"))); list_free(search_path); } } /* * Read an SQL script file into a string, and convert to database encoding */ static char * read_custom_script_file(const char *filename) { int src_encoding, dest_encoding = GetDatabaseEncoding(); bytea *content; char *src_str; char *dest_str; int len; FILE *fp; struct stat fst; size_t nbytes; /* * read_binary_file was made static in 9.5 so we'll reimplement the logic * here */ if ((fp = AllocateFile(filename, PG_BINARY_R)) == NULL) ereport(ERROR, (errcode_for_file_access(), errmsg("could not open file \"%s\" for reading: %m", filename))); if (fstat(fileno(fp), &fst) < 0) ereport(ERROR, (errcode_for_file_access(), errmsg("could not stat file \"%s\" %m", filename))); content = (bytea *) palloc((Size) fst.st_size + VARHDRSZ); nbytes = fread(VARDATA(content), 1, (size_t) fst.st_size, fp); if (ferror(fp)) ereport(ERROR, (errcode_for_file_access(), errmsg("could not read file \"%s\": %m", filename))); FreeFile(fp); SET_VARSIZE(content, nbytes + VARHDRSZ); /* use database encoding */ src_encoding = dest_encoding; /* make sure that source string is valid in the expected encoding */ len = VARSIZE_ANY_EXHDR(content); src_str = VARDATA_ANY(content); pg_verify_mbstr_len(src_encoding, src_str, len, false); /* convert the encoding to the database encoding */ dest_str = (char *) pg_do_encoding_conversion((unsigned char *) src_str, len, src_encoding, dest_encoding); /* if no conversion happened, we have to arrange for null termination */ if (dest_str == src_str) { dest_str = (char *) palloc(len + 1); memcpy(dest_str, src_str, len); dest_str[len] = '\0'; } return dest_str; } /* * Execute given SQL string. * * filename is used only to report errors. * * Note: it's tempting to just use SPI to execute the string, but that does * not work very well. The really serious problem is that SPI will parse, * analyze, and plan the whole string before executing any of it; of course * this fails if there are any planable statements referring to objects * created earlier in the script. A lesser annoyance is that SPI insists * on printing the whole string as errcontext in case of any error, and that * could be very long. */ static void execute_sql_string(const char *sql, const char *filename) { List *raw_parsetree_list; DestReceiver *dest; ListCell *lc1; MemoryContext temp_ctx = AllocSetContextCreate(CurrentMemoryContext, "temp_script_context", ALLOCSET_DEFAULT_SIZES); MemoryContext prev_ctx = MemoryContextSwitchTo(temp_ctx); /* * Parse the SQL string into a list of raw parse trees. */ raw_parsetree_list = pg_parse_query(sql); /* All output from SELECTs goes to the bit bucket */ dest = CreateDestReceiver(DestNone); /* * Do parse analysis, rule rewrite, planning, and execution for each raw * parsetree. We must fully execute each query before beginning parse * analysis on the next one, since there may be interdependencies. */ foreach(lc1, raw_parsetree_list) { RawStmt *parsetree = lfirst_node(RawStmt, lc1); List *stmt_list; ListCell *lc2; #if PG_MAJOR_VERSION >= 1500 stmt_list = pg_analyze_and_rewrite_fixedparams(parsetree, sql, NULL, 0, NULL ); #else stmt_list = pg_analyze_and_rewrite(parsetree, sql, NULL, 0 ,NULL ); #endif stmt_list = pg_plan_queries(stmt_list, #if PG_MAJOR_VERSION >= 1300 sql, #endif 0, NULL); foreach(lc2, stmt_list) { PlannedStmt *stmt = lfirst_node(PlannedStmt, lc2); if (IsA(stmt, TransactionStmt)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("transaction control statements are not allowed within an extension script"))); CommandCounterIncrement(); PushActiveSnapshot(GetTransactionSnapshot()); if (IsA(stmt, PlannedStmt) && ((PlannedStmt *) stmt)->utilityStmt == NULL) { QueryDesc *qdesc; qdesc = CreateQueryDesc((PlannedStmt *) stmt, sql, GetActiveSnapshot(), NULL, dest, NULL, NULL, 0); ExecutorStart(qdesc, 0); ExecutorRun(qdesc, ForwardScanDirection, 0 #if PG_MAJOR_VERSION < 1800 ,true #endif ); ExecutorFinish(qdesc); ExecutorEnd(qdesc); FreeQueryDesc(qdesc); } else { ProcessUtility(stmt, sql, #if PG_MAJOR_VERSION >= 1400 false, /* no need to copy */ #endif PROCESS_UTILITY_QUERY, NULL, NULL, dest, NULL); } PopActiveSnapshot(); } } /* Be sure to advance the command counter after the last script command */ CommandCounterIncrement(); MemoryContextSwitchTo(prev_ctx); MemoryContextDelete(temp_ctx); } /* * get_current_database_owner_name * * select rolname * from pg_roles u join pg_database d on d.datdba = u.oid * where datname = current_database(); */ static char * get_current_database_owner_name(void) { HeapTuple dbtuple; Oid owner; dbtuple = SearchSysCache1(DATABASEOID, MyDatabaseId); if (HeapTupleIsValid(dbtuple)) { owner = ((Form_pg_database) GETSTRUCT(dbtuple))->datdba; ReleaseSysCache(dbtuple); } else return NULL; return GetUserNameFromId(owner, false); } /* * Reject @-substitution values containing characters that could be used to * break out of quoting in the resulting script (CVE-2023-39417; upstream * fix cd5f2a3570). The check is applied to the raw, unquoted name because * quote_identifier() output always contains '"', which would trip the * check spuriously. */ static void check_substitution_value(const char *placeholder, const char *value) { static const char forbidden[] = "\"$'\\"; if (value == NULL) ereport(ERROR, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("invalid value for \"%s\" substitution", placeholder))); if (strpbrk(value, forbidden) != NULL) ereport(ERROR, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("invalid value for \"%s\" substitution", placeholder), errdetail("Value must not contain any of: %s", forbidden), errhint("Rename the object referenced by %s.", placeholder))); } /* * Execute given script */ void execute_custom_script(const char *filename, const char *schemaName) { int save_nestlevel; StringInfoData pathbuf; elog(DEBUG1, "Executing custom script \"%s\"", filename); /* * Force client_min_messages and log_min_messages to be at least WARNING, * so that we won't spam the user with useless NOTICE messages from common * script actions like creating shell types. * * We use the equivalent of a function SET option to allow the setting to * persist for exactly the duration of the script execution. guc.c also * takes care of undoing the setting on error. */ save_nestlevel = NewGUCNestLevel(); if (client_min_messages < WARNING) (void) set_config_option("client_min_messages", "warning", PGC_USERSET, PGC_S_SESSION, GUC_ACTION_SAVE, true, 0, false ); /* * log_min_messages was changed from a scalar int (PG <= 18) to something * more complex in PG 19. Set it unconditionally here for simplicity. */ (void) set_config_option("log_min_messages", "warning", PGC_SUSET, PGC_S_SESSION, GUC_ACTION_SAVE, true, 0, false); /* * Set up the search path to contain the target schema, then the schemas * of any prerequisite extensions, and nothing else. In particular this * makes the target schema be the default creation target namespace. * * Note: it might look tempting to use PushOverrideSearchPath for this, * but we cannot do that. We have to actually set the search_path GUC in * case the extension script examines or changes it. In any case, the * GUC_ACTION_SAVE method is just as convenient. */ initStringInfo(&pathbuf); appendStringInfoString(&pathbuf, quote_identifier(schemaName)); (void) set_config_option("search_path", pathbuf.data, PGC_USERSET, PGC_S_SESSION, GUC_ACTION_SAVE, true, 0, false); PG_TRY(); { char *c_sql = read_custom_script_file(filename); Datum t_sql; /* We use various functions that want to operate on text datums */ t_sql = CStringGetTextDatum(c_sql); /* * Reduce any lines beginning with "\echo" to empty. This allows * scripts to contain messages telling people not to run them via * psql, which has been found to be necessary due to old habits. */ t_sql = DirectFunctionCall4Coll(textregexreplace, C_COLLATION_OID, t_sql, CStringGetTextDatum("^\\\\echo.*$"), CStringGetTextDatum(""), CStringGetTextDatum("ng")); /* * substitute the target schema name for occurrences of @extschema@. */ check_substitution_value("@extschema@", schemaName); t_sql = DirectFunctionCall3Coll(replace_text, C_COLLATION_OID, t_sql, CStringGetTextDatum("@extschema@"), CStringGetTextDatum(schemaName)); /* * substitute the current user name for occurrences of @current_user@ */ { char *cur_user = GetUserNameFromId(GetUserId(), false); check_substitution_value("@current_user@", cur_user); t_sql = DirectFunctionCall3Coll(replace_text, C_COLLATION_OID, t_sql, CStringGetTextDatum("@current_user@"), CStringGetTextDatum(cur_user)); } /* * substitute the database owner for occurrences of @database_owner@ */ { char *db_owner = get_current_database_owner_name(); check_substitution_value("@database_owner@", db_owner); t_sql = DirectFunctionCall3Coll(replace_text, C_COLLATION_OID, t_sql, CStringGetTextDatum("@database_owner@"), CStringGetTextDatum(db_owner)); } /* And now back to C string */ c_sql = text_to_cstring(DatumGetTextPP(t_sql)); execute_sql_string(c_sql, filename); } PG_CATCH(); { PG_RE_THROW(); } PG_END_TRY(); /* * Restore the GUC variables we set above. */ AtEOXact_GUC(true, save_nestlevel); } dimitri-pgextwlist-ab92a4c/utils.h000066400000000000000000000023271522346476200174020ustar00rootroot00000000000000/* * PostgreSQL Extension WhiteList -- Dimitri Fontaine * * Author: Dimitri Fontaine * Licence: PostgreSQL * Copyright Dimitri Fontaine, 2011-2013 * * For a description of the features see the README.md file from the same * distribution. */ #ifndef __UTILS_H__ #define __UTILS_H__ #include "utils/builtins.h" #include "nodes/pg_list.h" #define MAXPGPATH 1024 extern char *extwlist_extensions; extern char *extwlist_custom_path; extern bool extwlist_restrict_to_database_owner; char *get_specific_custom_script_filename(const char *name, const char *when, const char *from_version, const char *version); char *get_generic_custom_script_filename(const char *name, const char *action, const char *when); char *get_custom_script_dir(const char *extname); void validate_custom_script_dir(const char *extname); char *get_extension_current_version(const char *extname); void fill_in_extension_properties(const char *extname, List *options, char **schema, char **old_version, char **new_version); void execute_custom_script(const char *schemaName, const char *filename); #endif