timescaledb/tsl/test/sql/data_node_bootstrap.sql
Sven Klemm c8b8516e46 Fix extension installation privilege escalation
TimescaleDB was vulnerable to a privilege escalation attack in
the extension installation script. An attacker could precreate
objects normally owned by the extension and get those objects
used in the installation script since the script would only try
to create them if they did not already exist. Thanks to Pedro
Gallegos for reporting the problem.

This patch changes the schema, table and function creation to fail
and abort the installation when the object already exists instead
of using the existing object.

Security: CVE-2022-24128
2022-02-09 17:53:20 +01:00

462 lines
16 KiB
PL/PgSQL

-- This file and its contents are licensed under the Timescale License.
-- Please see the included NOTICE for copyright information and
-- LICENSE-TIMESCALE for a copy of the license.
\c :TEST_DBNAME :ROLE_SUPERUSER;
CREATE OR REPLACE FUNCTION show_data_nodes()
RETURNS TABLE(data_node_name NAME, host TEXT, port INT, dbname NAME)
AS :TSL_MODULE_PATHNAME, 'ts_test_data_node_show' LANGUAGE C;
-- Fetch the encoding, collation, and ctype as quoted strings into
-- variables.
SELECT QUOTE_LITERAL(PG_ENCODING_TO_CHAR(encoding)) AS enc
, QUOTE_LITERAL(datcollate) AS coll
, QUOTE_LITERAL(datctype) AS ctype
FROM pg_database
WHERE datname = current_database()
\gset
\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER;
SELECT * FROM add_data_node('bootstrap_test', host => 'localhost', database => 'bootstrap_test');
-- Ensure database and extensions are installed and have the correct
-- encoding, ctype and collation.
\c bootstrap_test :ROLE_CLUSTER_SUPERUSER;
SELECT extname, nspname
FROM pg_extension e, pg_namespace n
WHERE e.extnamespace = n.oid
AND e.extname = 'timescaledb';
SELECT PG_ENCODING_TO_CHAR(encoding) = :enc
, datcollate = :coll
, datctype = :ctype
FROM pg_database
WHERE datname = current_database();
\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER;
-- After delete_data_node, the database and extension should still
-- exist on the data node
SELECT * FROM delete_data_node('bootstrap_test');
SELECT * FROM show_data_nodes();
\c bootstrap_test :ROLE_CLUSTER_SUPERUSER;
SELECT extname, nspname
FROM pg_extension e, pg_namespace n
WHERE e.extnamespace = n.oid
AND e.extname = 'timescaledb';
\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER;
\set ON_ERROR_STOP 0
-- Trying to add the data node again should fail, with or without
-- bootstrapping.
SELECT * FROM add_data_node('bootstrap_test', host => 'localhost', database => 'bootstrap_test', bootstrap=>false);
SELECT * FROM add_data_node('bootstrap_test', host => 'localhost', database => 'bootstrap_test');
\set ON_ERROR_STOP 0
DROP DATABASE bootstrap_test;
----------------------------------------------------------------------
-- Bootstrap the database and check that calling it without
-- bootstrapping does not find any problems.
\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER;
SELECT * FROM add_data_node('bootstrap_test', host => 'localhost',
database => 'bootstrap_test', bootstrap => true);
SELECT * FROM show_data_nodes();
SELECT * FROM delete_data_node('bootstrap_test');
\c bootstrap_test :ROLE_CLUSTER_SUPERUSER;
-- This should show dist_uuid row on the deleted node since that is
-- not removed by delete_data_node.
SELECT key FROM _timescaledb_catalog.metadata WHERE key = 'dist_uuid';
-- Delete the dist_uuid so that we can try to re-add it without
-- bootstrapping.
DELETE FROM _timescaledb_catalog.metadata WHERE key = 'dist_uuid';
\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER;
SELECT * FROM add_data_node('bootstrap_test', host => 'localhost',
database => 'bootstrap_test', bootstrap => false);
\set ON_ERROR_STOP 0
-- Dropping the database with delete_data_node should not work in a
-- transaction block since it is non-transactional.
BEGIN;
SELECT * FROM delete_data_node('bootstrap_test', drop_database => true);
ROLLBACK;
\set ON_ERROR_STOP 1
-- Using the drop_database option when there are active connections to
-- the data node should fail. But any connections in the current
-- session should be cleared when dropping the database. To test that
-- the connection is cleared, first create a connection in the
-- connection cache by inserting some data
CREATE TABLE conditions (time timestamptz, device int, temp float);
SELECT create_distributed_hypertable('conditions', 'time', 'device');
INSERT INTO conditions VALUES ('2021-12-01 10:30', 1, 20.3);
DROP TABLE conditions;
-- Now drop the data node and it should clear the connection from the
-- cache first
SELECT * FROM delete_data_node('bootstrap_test', drop_database => true);
\set ON_ERROR_STOP 0
-- Dropping the database now should fail since it no longer exists
DROP DATABASE bootstrap_test;
\set ON_ERROR_STOP 1
-- Adding the data node again should work
SELECT * FROM add_data_node('bootstrap_test', host => 'localhost',
database => 'bootstrap_test', bootstrap => true);
-- Now drop the database manually before using the drop_database option
DROP DATABASE bootstrap_test;
\set ON_ERROR_STOP 0
-- Expect an error since the database does not exist.
SELECT * FROM delete_data_node('bootstrap_test', drop_database => true);
\set ON_ERROR_STOP 1
-- Delete it without the drop_database option set since the database
-- was manually deleted.
SELECT * FROM delete_data_node('bootstrap_test');
----------------------------------------------------------------------
-- Do a manual bootstrap of the data node and check that it can be
-- added.
\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER;
CREATE DATABASE bootstrap_test OWNER :ROLE_CLUSTER_SUPERUSER;
\c bootstrap_test :ROLE_CLUSTER_SUPERUSER
SET client_min_messages TO ERROR;
CREATE EXTENSION timescaledb;
SET client_min_messages TO NOTICE;
\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER
SELECT * FROM add_data_node('bootstrap_test', host => 'localhost',
database => 'bootstrap_test', bootstrap => false);
SELECT * FROM delete_data_node('bootstrap_test');
DROP DATABASE bootstrap_test;
----------------------------------------------------------------------
-- Do a manual bootstrap of the data node and check that it can be
-- added even when bootstrap is true. This is to check that we can
-- bootstrap a database with an extension already installed.
\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER;
CREATE DATABASE bootstrap_test OWNER :ROLE_CLUSTER_SUPERUSER;
\c bootstrap_test :ROLE_CLUSTER_SUPERUSER;
SET client_min_messages TO ERROR;
CREATE EXTENSION timescaledb;
SET client_min_messages TO NOTICE;
\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER;
SELECT * FROM add_data_node('bootstrap_test', host => 'localhost',
database => 'bootstrap_test', bootstrap => true);
SELECT * FROM delete_data_node('bootstrap_test');
DROP DATABASE bootstrap_test;
----------------------------------------------------------------------
-- Create a database and check that a mismatching encoding is caught
-- when bootstrapping (since it will skip creating the database).
\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER;
CREATE DATABASE bootstrap_test
ENCODING SQL_ASCII
LC_COLLATE 'C'
LC_CTYPE 'C'
TEMPLATE template0
OWNER :ROLE_CLUSTER_SUPERUSER;
\set ON_ERROR_STOP 0
SELECT * FROM add_data_node('bootstrap_test', host => 'localhost',
database => 'bootstrap_test', bootstrap => true);
\set ON_ERROR_STOP 1
DROP DATABASE bootstrap_test;
----------------------------------------------------------------------
-- Do a manual bootstrap of the data but check that a mismatching
-- encoding, ctype, or collation will be caught.
\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER;
-- Pick an alternative locale for testing from the list of installed
-- collations. This locale needs to be differnt from the current
-- database's locale. We pick it from the list of collations in a
-- platform agnostic way since, e.g., Linux and Windows have very
-- different locale names.
SELECT QUOTE_LITERAL(c.collctype) AS other_locale
FROM pg_collation c, pg_database d
WHERE c.collencoding = d.encoding
AND d.datctype != c.collctype
AND d.datname = current_database()
ORDER BY c.oid DESC
LIMIT 1
\gset
CREATE DATABASE bootstrap_test
ENCODING SQL_ASCII
LC_COLLATE 'C'
LC_CTYPE 'C'
TEMPLATE template0
OWNER :ROLE_CLUSTER_SUPERUSER;
\c bootstrap_test :ROLE_CLUSTER_SUPERUSER;
SET client_min_messages TO ERROR;
CREATE EXTENSION timescaledb;
SET client_min_messages TO NOTICE;
\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER;
\set ON_ERROR_STOP 0
SELECT * FROM add_data_node('bootstrap_test', host => 'localhost',
database => 'bootstrap_test', bootstrap => false);
\set ON_ERROR_STOP 1
DROP DATABASE bootstrap_test;
CREATE DATABASE bootstrap_test
ENCODING :"enc"
LC_COLLATE :other_locale
LC_CTYPE :ctype
TEMPLATE template0
OWNER :ROLE_CLUSTER_SUPERUSER;
\c bootstrap_test :ROLE_CLUSTER_SUPERUSER;
SET client_min_messages TO ERROR;
CREATE EXTENSION timescaledb;
SET client_min_messages TO NOTICE;
\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER;
\set ON_ERROR_STOP 0
SELECT * FROM add_data_node('bootstrap_test', host => 'localhost',
database => 'bootstrap_test', bootstrap => false);
\set ON_ERROR_STOP 1
DROP DATABASE bootstrap_test;
CREATE DATABASE bootstrap_test
ENCODING :"enc"
LC_COLLATE :coll
LC_CTYPE :other_locale
TEMPLATE template0
OWNER :ROLE_CLUSTER_SUPERUSER;
\c bootstrap_test :ROLE_CLUSTER_SUPERUSER;
SET client_min_messages TO ERROR;
CREATE EXTENSION timescaledb;
SET client_min_messages TO NOTICE;
\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER;
\set ON_ERROR_STOP 0
SELECT * FROM add_data_node('bootstrap_test', host => 'localhost',
database => 'bootstrap_test', bootstrap => false);
\set ON_ERROR_STOP 1
DROP DATABASE bootstrap_test;
-----------------------------------------------------------------------
-- Bootstrap the database and remove it. Check that the missing
-- database is caught when adding the node and not bootstrapping.
\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER;
SELECT * FROM add_data_node('bootstrap_test', host => 'localhost',
database => 'bootstrap_test', bootstrap => true);
SELECT * FROM show_data_nodes();
SELECT * FROM delete_data_node('bootstrap_test');
DROP DATABASE bootstrap_test;
\set ON_ERROR_STOP 0
SELECT * FROM add_data_node('bootstrap_test', host => 'localhost',
database => 'bootstrap_test', bootstrap => false);
\set ON_ERROR_STOP 1
-----------------------------------------------------------------------
-- Bootstrap the database and remove the extension.
--
-- Check that adding the data node and not bootstrapping will fail
-- indicating that the extension is missing.
\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER
SELECT * FROM add_data_node('bootstrap_test', host => 'localhost',
database => 'bootstrap_test', bootstrap => true);
SELECT * FROM delete_data_node('bootstrap_test');
\c bootstrap_test :ROLE_SUPERUSER;
SELECT extname FROM pg_extension WHERE extname = 'timescaledb';
DROP EXTENSION timescaledb CASCADE;
SELECT extname FROM pg_extension WHERE extname = 'timescaledb';
\set ON_ERROR_STOP 0
\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER;
SELECT * FROM add_data_node('bootstrap_test', host => 'localhost',
database => 'bootstrap_test', bootstrap => false);
\set ON_ERROR_STOP 1
DROP DATABASE bootstrap_test;
-----------------------------------------------------------------------
-- Create a new access node manually so that we can set a specific
-- schema for the access node and then bootstrap a data node partially
-- with a non-public schema so that we can see that an error is
-- generated.
\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER
CREATE DATABASE access_node OWNER :ROLE_CLUSTER_SUPERUSER;
\c access_node :ROLE_CLUSTER_SUPERUSER
SET client_min_messages TO ERROR;
CREATE SCHEMA ts_non_default AUTHORIZATION :ROLE_CLUSTER_SUPERUSER;
CREATE EXTENSION timescaledb WITH SCHEMA ts_non_default CASCADE;
SET client_min_messages TO NOTICE;
-- Show the schema for the extension to verify that it is not public.
SELECT extname FROM pg_extension WHERE extname = 'timescaledb';
\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER
CREATE DATABASE bootstrap_test OWNER :ROLE_CLUSTER_SUPERUSER;
\c bootstrap_test :ROLE_CLUSTER_SUPERUSER
SET client_min_messages TO ERROR;
CREATE SCHEMA ts_non_default AUTHORIZATION :ROLE_CLUSTER_SUPERUSER;
SET client_min_messages TO NOTICE;
\c access_node :ROLE_CLUSTER_SUPERUSER
-- Add data node and delete it under error suppression. We want to
-- avoid later tests to have random failures because the add succeeds.
\set ON_ERROR_STOP 0
SELECT * FROM ts_non_default.add_data_node(
'bootstrap_test', host => 'localhost',
database => 'bootstrap_test', bootstrap => true);
SELECT * FROM ts_non_default.delete_data_node('bootstrap_test');
\set ON_ERROR_STOP 1
\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER
DROP DATABASE bootstrap_test;
DROP DATABASE access_node;
----------------------------------------------------------------------
-- Test for ongoing transaction
\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER
BEGIN;
\set ON_ERROR_STOP 0
SELECT * FROM add_data_node('bootstrap_test', host => 'localhost', database => 'bootstrap_test');
\set ON_ERROR_STOP 1
COMMIT;
SELECT * FROM show_data_nodes();
---------------------------------------------------------------------
-- Test unusual database names
--
-- bootstrap_database = 'postgres'
CREATE FUNCTION test_database_name(name TEXT) RETURNS void AS $$
BEGIN
PERFORM add_data_node('_test1', host => 'localhost', database => name);
PERFORM delete_data_node('_test1');
END;
$$ LANGUAGE plpgsql;
\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER;
SELECT test_database_name('Unusual Name');
SELECT test_database_name(U&'\0441\043B\043E\043D');
DROP DATABASE "Unusual Name";
DROP DATABASE U&"\0441\043B\043E\043D";
-- Test Access Node DATABASE DROP NOTICE message
--
-- Make sure the NOTICE message not shown on a DROP DATABASE error
\set ON_ERROR_STOP 0
DROP DATABASE :TEST_DBNAME;
\set ON_ERROR_STOP 1
CREATE DATABASE drop_db_test;
\c drop_db_test :ROLE_CLUSTER_SUPERUSER;
SET client_min_messages TO ERROR;
CREATE EXTENSION timescaledb;
SET client_min_messages TO NOTICE;
-- No security label exists
SELECT label FROM pg_shseclabel
WHERE objoid = (SELECT oid from pg_database WHERE datname = 'drop_db_test') AND
provider = 'timescaledb';
SELECT * FROM add_data_node('drop_db_test_dn', host => 'localhost', database => 'drop_db_test_dn');
-- Make sure security label is created
SELECT substr(label, 0, 10) || ':uuid'
FROM pg_shseclabel
WHERE objoid = (SELECT oid from pg_database WHERE datname = 'drop_db_test') AND
provider = 'timescaledb';
\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER;
-- Check that timescaledb security label cannot be used directly. To
-- support pg_dump, we do not print an error when a proper label is
-- used, but print an error if something that doesn't look like a
-- distributed uuid is used.
SECURITY LABEL FOR timescaledb
ON DATABASE drop_db_test
IS 'dist_uuid:4ab3b1bc-438f-11ec-8919-23804e22321a';
\set ON_ERROR_STOP 0
\set VERBOSITY default
-- No colon
SECURITY LABEL FOR timescaledb
ON DATABASE drop_db_test
IS 'bad_label';
-- Bad tag, but still an UUID
SECURITY LABEL FOR timescaledb
ON DATABASE drop_db_test
IS 'uuid:4ab3b1bc-438f-11ec-8919-23804e22321a';
-- Length is not correct
SECURITY LABEL FOR timescaledb
ON DATABASE drop_db_test
IS 'dist_uuid:4ab3b1bcd-438f-11ec-8919-23804e2232';
SECURITY LABEL FOR timescaledb
ON DATABASE drop_db_test
IS 'dist_uuid:4ab3b1bcd-438f-11ec-8919-23804e223215';
-- Length is correct, but it contains something that is not a
-- hexadecimal digit.
SECURITY LABEL FOR timescaledb
ON DATABASE drop_db_test
IS 'dist_uuid:4ab3b1bcd-4x8f-11ec-8919-23804e22321';
-- Total length is correct, but not the right number of hyphens.
SECURITY LABEL FOR timescaledb
ON DATABASE drop_db_test
IS 'dist_uuid:4ab3-1bcd-438f-11ec-8919-23804e22321';
SECURITY LABEL FOR timescaledb
ON DATABASE drop_db_test
IS 'dist_uuid:4ab3b1bcd438f-11ec-8919-23804e223213';
-- Total length is correct, but length of groups is not
SECURITY LABEL FOR timescaledb
ON DATABASE drop_db_test
IS 'dist_uuid:4ab3b1bcd-438f-11ec-8919-23804e22321';
SECURITY LABEL FOR timescaledb
ON DATABASE drop_db_test
IS 'dist_uuid:4ab3b1bc-438f-11ec-891-23804e22321ab';
SECURITY LABEL FOR timescaledb
ON DATABASE drop_db_test
IS 'dist_uuid:4ab3b1bc-438f-11e-8919-23804e22321ab';
SECURITY LABEL FOR timescaledb
ON DATABASE drop_db_test
IS 'dist_uuid:4ab3b1bc-438-11ec-8919-23804e22321ab';
SECURITY LABEL FOR timescaledb
ON DATABASE drop_db_test
IS 'dist_uuid:4ab3b1bca-438f-11ec-8919-23804e22321';
\set VERBOSITY terse
\set ON_ERROR_STOP 1
-- Check that security label functionality is working
CREATE TABLE seclabel_test(id int);
SECURITY LABEL ON TABLE seclabel_test IS 'label';
DROP TABLE seclabel_test;
-- This will generate NOTICE message
DROP DATABASE drop_db_test;
DROP DATABASE drop_db_test_dn;
-- Ensure label is deleted after the DROP
SELECT label FROM pg_shseclabel
WHERE objoid = (SELECT oid from pg_database WHERE datname = 'drop_db_test') AND
provider = 'timescaledb';