From 7a62c7d812af39a2fb8d86e02fad05a685c289cb Mon Sep 17 00:00:00 2001 From: Sarah Hoffmann Date: Mon, 9 Feb 2026 16:26:15 +0100 Subject: [PATCH 1/5] sanity check class names before inserting into classtype tables The subsequent INSERT is done on an unqouted table name, making in theory an SQL injection through an OSM value possible. In practise this cannot happen because we check for the existance of the table. During the creation of the classtype tables there is a sanity check in place to disallow any table names that consist of anything other than alphanumeric characters. --- lib-sql/functions/placex_triggers.sql | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lib-sql/functions/placex_triggers.sql b/lib-sql/functions/placex_triggers.sql index 244fe90c..3f266292 100644 --- a/lib-sql/functions/placex_triggers.sql +++ b/lib-sql/functions/placex_triggers.sql @@ -672,7 +672,7 @@ CREATE OR REPLACE FUNCTION placex_insert() AS $$ DECLARE postcode TEXT; - result BOOLEAN; + result INT; is_area BOOLEAN; country_code VARCHAR(2); diameter FLOAT; @@ -777,11 +777,12 @@ BEGIN -- add to tables for special search - -- Note: won't work on initial import because the classtype tables - -- do not yet exist. It won't hurt either. classtable := 'place_classtype_' || NEW.class || '_' || NEW.type; - SELECT count(*)>0 FROM pg_tables WHERE tablename = classtable and schemaname = current_schema() INTO result; - IF result THEN + SELECT count(*) INTO result + FROM pg_tables + WHERE classtable NOT SIMILAR TO '%\W%' + AND tablename = classtable and schemaname = current_schema(); + IF result > 0 THEN EXECUTE 'INSERT INTO ' || classtable::regclass || ' (place_id, centroid) VALUES ($1,$2)' USING NEW.place_id, NEW.centroid; END IF; @@ -1337,6 +1338,7 @@ CREATE OR REPLACE FUNCTION placex_delete() AS $$ DECLARE b BOOLEAN; + result INT; classtable TEXT; BEGIN -- RAISE WARNING 'placex_delete % %',OLD.osm_type,OLD.osm_id; @@ -1395,8 +1397,12 @@ BEGIN -- remove from tables for special search classtable := 'place_classtype_' || OLD.class || '_' || OLD.type; - SELECT count(*)>0 FROM pg_tables WHERE tablename = classtable and schemaname = current_schema() INTO b; - IF b THEN + SELECT count(*) INTO result + FROM pg_tables + WHERE classtable NOT SIMILAR TO '%\W%' + AND tablename = classtable and schemaname = current_schema(); + + IF result > 0 THEN EXECUTE 'DELETE FROM ' || classtable::regclass || ' WHERE place_id = $1' USING OLD.place_id; END IF; From e17d0cb5cf314a5bcd66a9bfee16e760ce26cde7 Mon Sep 17 00:00:00 2001 From: Sarah Hoffmann Date: Mon, 9 Feb 2026 21:07:31 +0100 Subject: [PATCH 2/5] only allow alphanumeric and dash in DATABASE_WEBUSER This variable is used a lot in raw SQL. Avoid injection issues. --- src/nominatim_db/config.py | 7 ++++++- test/python/config/test_config.py | 8 ++++---- test/python/tools/test_database_import.py | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/nominatim_db/config.py b/src/nominatim_db/config.py index 0742d019..2cda7892 100644 --- a/src/nominatim_db/config.py +++ b/src/nominatim_db/config.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2025 by the Nominatim developer community. +# Copyright (C) 2026 by the Nominatim developer community. # For a full list of authors see the git log. """ Nominatim configuration accessor. @@ -12,6 +12,7 @@ import importlib.util import logging import os import sys +import re from pathlib import Path import json import yaml @@ -80,6 +81,10 @@ class Configuration: self.lib_dir = _LibDirs() self._private_plugins: Dict[str, object] = {} + if re.fullmatch(r'[\w-]+', self.DATABASE_WEBUSER) is None: + raise UsageError("Misconfigured DATABASE_WEBUSER. " + "Only alphnumberic characters, - and _ are allowed.") + def set_libdirs(self, **kwargs: StrPath) -> None: """ Set paths to library functions and data. """ diff --git a/test/python/config/test_config.py b/test/python/config/test_config.py index 34e7acd7..555bc4e7 100644 --- a/test/python/config/test_config.py +++ b/test/python/config/test_config.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2025 by the Nominatim developer community. +# Copyright (C) 2026 by the Nominatim developer community. # For a full list of authors see the git log. """ Test for loading dotenv configuration. @@ -68,13 +68,13 @@ def test_prefer_os_environ_over_project_setting(make_config, monkeypatch, tmp_pa def test_prefer_os_environ_can_unset_project_setting(make_config, monkeypatch, tmp_path): envfile = tmp_path / '.env' - envfile.write_text('NOMINATIM_DATABASE_WEBUSER=apache\n', encoding='utf-8') + envfile.write_text('NOMINATIM_OSM2PGSQL_BINARY=osm2pgsql\n', encoding='utf-8') - monkeypatch.setenv('NOMINATIM_DATABASE_WEBUSER', '') + monkeypatch.setenv('NOMINATIM_OSM2PGSQL_BINARY', '') config = make_config(tmp_path) - assert config.DATABASE_WEBUSER == '' + assert config.OSM2PGSQL_BINARY == '' def test_get_os_env_add_defaults(make_config, monkeypatch): diff --git a/test/python/tools/test_database_import.py b/test/python/tools/test_database_import.py index 6bae0389..eab747a5 100644 --- a/test/python/tools/test_database_import.py +++ b/test/python/tools/test_database_import.py @@ -62,7 +62,7 @@ class TestDatabaseSetup: def test_create_db_missing_ro_user(self): with pytest.raises(UsageError, match='Missing read-only user.'): database_import.setup_database_skeleton(f'dbname={self.DBNAME}', - rouser='sdfwkjkjgdugu2;jgsafkljas;') + rouser='sdfwkjkjgdugu2jgsafkljas') def test_setup_extensions_old_postgis(self, monkeypatch): monkeypatch.setattr(database_import, 'POSTGIS_REQUIRED_VERSION', (50, 50)) From 73590baf15d3ff6679a5427dd378840cd8fad01a Mon Sep 17 00:00:00 2001 From: Sarah Hoffmann Date: Mon, 9 Feb 2026 21:21:18 +0100 Subject: [PATCH 3/5] use psycopg.sql for SQL building in tokenizer --- src/nominatim_db/tokenizer/icu_tokenizer.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/nominatim_db/tokenizer/icu_tokenizer.py b/src/nominatim_db/tokenizer/icu_tokenizer.py index 5d90bb27..297f637e 100644 --- a/src/nominatim_db/tokenizer/icu_tokenizer.py +++ b/src/nominatim_db/tokenizer/icu_tokenizer.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2025 by the Nominatim developer community. +# Copyright (C) 2026 by the Nominatim developer community. # For a full list of authors see the git log. """ Tokenizer implementing normalisation as used before Nominatim 4 but using @@ -294,13 +294,12 @@ class ICUTokenizer(AbstractTokenizer): with connect(self.dsn) as conn: drop_tables(conn, 'word') with conn.cursor() as cur: - cur.execute(f"ALTER TABLE {old} RENAME TO word") - for idx in ('word_token', 'word_id'): - cur.execute(f"""ALTER INDEX idx_{old}_{idx} - RENAME TO idx_word_{idx}""") - for name, _ in WORD_TYPES: - cur.execute(f"""ALTER INDEX idx_{old}_{name} - RENAME TO idx_word_{name}""") + cur.execute(pysql.SQL("ALTER TABLE {} RENAME TO word") + .format(pysql.Identifier(old))) + for idx in ['word_token', 'word_id'] + [n[0] for n in WORD_TYPES]: + cur.execute(pysql.SQL("ALTER INDEX {} RENAME TO {}") + .format(pysql.Identifier(f"idx_{old}_{idx}"), + pysql.Identifier(f"idx_word_{idx}"))) conn.commit() From d10d70944d3e35c48da7b0c697b69081d76bed88 Mon Sep 17 00:00:00 2001 From: Sarah Hoffmann Date: Mon, 9 Feb 2026 21:25:01 +0100 Subject: [PATCH 4/5] avoid f-strings in SQL creation in tests --- test/python/conftest.py | 6 ++++-- test/python/tools/test_database_import.py | 8 +++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/test/python/conftest.py b/test/python/conftest.py index b2fe9d4a..2f19ed4c 100644 --- a/test/python/conftest.py +++ b/test/python/conftest.py @@ -60,7 +60,7 @@ def temp_db(monkeypatch): with psycopg.connect(dbname='postgres', autocommit=True) as conn: with conn.cursor() as cur: - cur.execute('DROP DATABASE IF EXISTS {}'.format(name)) + cur.execute(pysql.SQL('DROP DATABASE IF EXISTS') + pysql.Identifier(name)) @pytest.fixture @@ -104,7 +104,9 @@ def table_factory(temp_db_conn): """ def mk_table(name, definition='id INT', content=None): with psycopg.ClientCursor(temp_db_conn) as cur: - cur.execute('CREATE TABLE {} ({})'.format(name, definition)) + cur.execute(pysql.SQL("CREATE TABLE {} ({})") + .format(pysql.Identifier(name), + pysql.SQL(definition))) if content: sql = pysql.SQL("INSERT INTO {} VALUES ({})")\ .format(pysql.Identifier(name), diff --git a/test/python/tools/test_database_import.py b/test/python/tools/test_database_import.py index eab747a5..221e4fba 100644 --- a/test/python/tools/test_database_import.py +++ b/test/python/tools/test_database_import.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2025 by the Nominatim developer community. +# Copyright (C) 2026 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for functions to import a new database. @@ -25,12 +25,14 @@ class TestDatabaseSetup: def setup_nonexistant_db(self): with psycopg.connect(dbname='postgres', autocommit=True) as conn: with conn.cursor() as cur: - cur.execute(f'DROP DATABASE IF EXISTS {self.DBNAME}') + cur.execute(pysql.SQL('DROP DATABASE IF EXISTS ') + + pysql.Identifier(self.DBNAME)) yield True with conn.cursor() as cur: - cur.execute(f'DROP DATABASE IF EXISTS {self.DBNAME}') + cur.execute(pysql.SQL('DROP DATABASE IF EXISTS ') + + pysql.Identifier(self.DBNAME)) @pytest.fixture def cursor(self): From 7a3ea55f3d9546c89064c07ffc741eb1e819d15e Mon Sep 17 00:00:00 2001 From: Sarah Hoffmann Date: Tue, 10 Feb 2026 11:33:04 +0100 Subject: [PATCH 5/5] ignore tables with odd names in SQLPreprocessor --- src/nominatim_db/db/sql_preprocessor.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/nominatim_db/db/sql_preprocessor.py b/src/nominatim_db/db/sql_preprocessor.py index 4424b3d8..12c3de26 100644 --- a/src/nominatim_db/db/sql_preprocessor.py +++ b/src/nominatim_db/db/sql_preprocessor.py @@ -2,12 +2,13 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2026 by the Nominatim developer community. # For a full list of authors see the git log. """ Preprocessing of SQL files. """ from typing import Set, Dict, Any, cast +import re import jinja2 @@ -34,7 +35,9 @@ def _get_tables(conn: Connection) -> Set[str]: with conn.cursor() as cur: cur.execute("SELECT tablename FROM pg_tables WHERE schemaname = 'public'") - return set((row[0] for row in list(cur))) + # paranoia check: make sure we don't get table names that cause + # an SQL injection later + return {row[0] for row in list(cur) if re.fullmatch(r'\w+', row[0])} def _get_middle_db_format(conn: Connection, tables: Set[str]) -> str: