diff --git a/CHANGELOG.md b/CHANGELOG.md index 47f72658c..45c2a88e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. From versio ## Unreleased +### Added + +- Log error when `db-schemas` config contains schema `pg_catalog` or `information_schema` by @taimoorzaeem in #4359 + ### Fixed - Fix misleading logs on unsupported PostgreSQL versions by @taimoorzaeem in #4519 diff --git a/docs/references/api/schemas.rst b/docs/references/api/schemas.rst index 398f8cad7..63cc78a83 100644 --- a/docs/references/api/schemas.rst +++ b/docs/references/api/schemas.rst @@ -5,6 +5,10 @@ Schemas PostgREST can expose a single or multiple schema's tables, views and functions. The :ref:`active database role ` must have the usage privilege on the schemas to access them. +.. important:: + + ``pg_catalog`` and ``information_schema`` are not allowed in :ref:`db-schemas`. This is done to prevent leaking sensitive information and hence they cannot be accessed directly. If you wish to expose objects of these schemas, expose another schema that contains wrapper views or functions over ``pg_catalog`` or ``information_schema`` objects. + Single schema ------------- diff --git a/src/PostgREST/Config.hs b/src/PostgREST/Config.hs index 00562e9cf..ecdac6a7b 100644 --- a/src/PostgREST/Config.hs +++ b/src/PostgREST/Config.hs @@ -274,8 +274,7 @@ parser optPath env dbSettings roleSettings roleIsolationLvl = <*> (fromMaybe True <$> optBool "db-prepared-statements") <*> (fmap toQi <$> optWithAlias (optString "db-root-spec") (optString "root-spec")) - <*> (fromList . maybe ["public"] splitOnCommas <$> optWithAlias (optString "db-schemas") - (optString "db-schema")) + <*> parseDbSchemas "db-schemas" "db-schema" <*> (fromMaybe True <$> optBool "db-config") <*> (fmap toQi <$> optString "db-pre-config") <*> parseTxEnd "db-tx-end" snd @@ -329,6 +328,18 @@ parser optPath env dbSettings roleSettings roleIsolationLvl = Just asp | asp == serverPort -> fail "admin-server-port cannot be the same as server-port" | otherwise -> pure $ Just asp + parseDbSchemas :: C.Key -> C.Key -> C.Parser C.Config (NonEmpty Text) + parseDbSchemas k al = + optWithAlias (optString k) (optString al) >>= \case + Nothing -> pure $ fromList ["public"] + Just s + | "pg_catalog" `elem` schemas -> fail (errMsg "pg_catalog") + | "information_schema" `elem` schemas -> fail (errMsg "information_schema") + | otherwise -> pure $ fromList schemas + where + schemas = splitOnCommas s + errMsg x = ("db-schemas does not allow schema: '" <> x <> "'") + parseSocketFileMode :: C.Key -> C.Parser C.Config FileMode parseSocketFileMode k = optString k >>= \case diff --git a/test/io/fixtures/fixtures.yaml b/test/io/fixtures/fixtures.yaml index ee00182cc..640a584b4 100644 --- a/test/io/fixtures/fixtures.yaml +++ b/test/io/fixtures/fixtures.yaml @@ -237,3 +237,7 @@ specialhostvalues: - '*6' - '!6' - '*' + +restrictedschemas: + - 'pg_catalog' + - 'information_schema' diff --git a/test/io/test_cli.py b/test/io/test_cli.py index df33547fe..8135fcce2 100644 --- a/test/io/test_cli.py +++ b/test/io/test_cli.py @@ -286,6 +286,21 @@ def test_jwt_secret_min_length(defaultenv): assert "The JWT secret must be at least 32 characters long." in error +@pytest.mark.parametrize("restricted_schema", FIXTURES["restrictedschemas"]) +def test_restricted_db_schemas(restricted_schema, defaultenv): + "Should print error when db-schemas config contain pg_catalog or information_schema" + + # test when single schema is given in db-schemas + env = {**defaultenv, "PGRST_DB_SCHEMAS": restricted_schema} + error = cli(["--dump-config"], env=env, expect_error=True) + assert f"db-schemas does not allow schema: '{restricted_schema}'" in error + + # test when multiple schemas are given in db-schemas + env = {**defaultenv, "PGRST_DB_SCHEMAS": f"public, {restricted_schema}"} + error = cli(["--dump-config"], env=env, expect_error=True) + assert f"db-schemas does not allow schema: '{restricted_schema}'" in error + + # TODO: Improve readability of "--ready" healthcheck tests @pytest.mark.parametrize("host", ["127.0.0.1", "::1"], ids=["IPv4", "IPv6"]) def test_cli_ready_flag_success(host, defaultenv):