Files
GTI_GERENCIADOR_FISCAL/tests/test_catalog.py
T

184 lines
7.6 KiB
Python

"""Mission 00D Phase 8: prove arbitrary SQL cannot reach execution through
the closed catalog API, and that legitimate Firebird constructs used by
the project's own internal queries are not rejected.
``FirebirdCatalog`` has no ``execute``/``query``/``raw_execute`` method at
all, so most classic SQL-injection payloads simply have nowhere to go --
these tests prove that structurally (no method exists to try them on) and
prove that the one dynamic operation, ``sample_relation_rows``, rejects
every malicious relation/column value because it is validated against the
catalog itself, never interpreted as SQL text.
"""
import pytest
from gti_discovery.catalog import MAX_SAMPLE_ROWS, FirebirdCatalog, _quote_identifier
from gti_discovery.firebird_queries import _ADAPTER_REGISTRATION_TOKEN, _register_approved_adapter
from gti_discovery.read_only import validate_read_only
class _RecordingAdapter:
read_only = True
def __init__(self, rows_by_call=None):
self.calls: list[tuple[str, tuple]] = []
self._rows_by_call = rows_by_call or {}
def execute(self, sql, params=()):
self.calls.append((sql, params))
return self._rows_by_call.get(len(self.calls) - 1, [])
_register_approved_adapter(_RecordingAdapter, _ADAPTER_REGISTRATION_TOKEN)
def _catalog_with_relations(relations, fields_by_relation):
"""A FirebirdCatalog whose relations/fields queries return fixture rows."""
relation_rows = [{"RELATION_NAME": name, "RELATION_ID": i, "SYSTEM_FLAG": 0} for i, name in enumerate(relations)]
field_rows = [
{"RELATION_NAME": relation, "FIELD_NAME": field, "FIELD_POSITION": i}
for relation, fields in fields_by_relation.items()
for i, field in enumerate(fields)
]
adapter = _RecordingAdapter({0: relation_rows, 1: field_rows})
catalog = FirebirdCatalog(adapter)
return catalog, adapter
# --- Phase 8: closed API surface -----------------------------------------
def test_no_arbitrary_sql_method_exists_at_all():
for forbidden in ("execute", "query", "raw_execute", "execute_statement", "run_sql", "run_query"):
assert not hasattr(FirebirdCatalog, forbidden)
# --- Phase 8: relation/column must be catalog-known, never free text -----
@pytest.mark.parametrize("malicious_relation", [
"PRODUCTS\"; DROP TABLE X; --",
"PRODUCTS' OR '1'='1",
'PRODUCTS" ("EAN"',
"PRODUCTS/*comment*/",
"PRODUCTS UNION SELECT * FROM RDB$USERS",
"SOME_PROCEDURE(1,2,3)",
'"SOME_UDF"(1)',
"PRODUCTS; SELECT * FROM RDB$USERS",
"(SELECT PASSWORD FROM USERS)",
"PRODUCTS AS X UNION SELECT 1",
])
def test_sample_relation_rows_rejects_unknown_or_malicious_relation(malicious_relation):
catalog, adapter = _catalog_with_relations(["PRODUCTS"], {"PRODUCTS": ["ID", "EAN"]})
with pytest.raises(ValueError):
catalog.sample_relation_rows(malicious_relation, ["ID"], 1)
# never reached the driver with a sampling query (only the relations
# bootstrap call, if any, is allowed to have happened)
sampling_calls = [c for c in adapter.calls if "FIRST" in c[0].upper()]
assert sampling_calls == []
@pytest.mark.parametrize("malicious_column", [
"EAN\"; DROP TABLE X; --",
"EAN' OR '1'='1",
"EAN, PASSWORD",
"EAN) UNION SELECT PASSWORD FROM USERS --",
"COUNT(*)",
"(SELECT 1)",
])
def test_sample_relation_rows_rejects_unknown_or_malicious_column(malicious_column):
catalog, adapter = _catalog_with_relations(["PRODUCTS"], {"PRODUCTS": ["ID", "EAN"]})
with pytest.raises(ValueError):
catalog.sample_relation_rows("PRODUCTS", [malicious_column], 1)
sampling_calls = [c for c in adapter.calls if "FIRST" in c[0].upper()]
assert sampling_calls == []
def test_sample_relation_rows_accepts_legitimate_relation_and_columns():
catalog, adapter = _catalog_with_relations(["PRODUCTS"], {"PRODUCTS": ["ID", "EAN"]})
catalog.sample_relation_rows("PRODUCTS", ["ID", "EAN"], 5)
sql, params = adapter.calls[-1]
assert sql == 'SELECT FIRST 5 "ID", "EAN" FROM "PRODUCTS"'
assert params == ()
@pytest.mark.parametrize("limit", [0, -1, 51, 10 ** 9, "5", 5.0, True])
def test_sample_relation_rows_rejects_bad_limit(limit):
catalog, _ = _catalog_with_relations(["PRODUCTS"], {"PRODUCTS": ["ID"]})
with pytest.raises(ValueError):
catalog.sample_relation_rows("PRODUCTS", ["ID"], limit)
def test_sample_relation_rows_limit_ceiling_is_enforced():
catalog, _ = _catalog_with_relations(["PRODUCTS"], {"PRODUCTS": ["ID"]})
catalog.sample_relation_rows("PRODUCTS", ["ID"], MAX_SAMPLE_ROWS)
with pytest.raises(ValueError):
catalog.sample_relation_rows("PRODUCTS", ["ID"], MAX_SAMPLE_ROWS + 1)
def test_sample_relation_rows_never_accepts_empty_columns():
catalog, _ = _catalog_with_relations(["PRODUCTS"], {"PRODUCTS": ["ID"]})
with pytest.raises(ValueError):
catalog.sample_relation_rows("PRODUCTS", [], 1)
def test_quote_identifier_doubles_embedded_quotes():
assert _quote_identifier('WEIRD"NAME') == '"WEIRD""NAME"'
# --- Phase 5: procedures are metadata-only, never executable -------------
def test_get_procedures_returns_metadata_never_a_callable():
adapter = _RecordingAdapter({0: [{"PROCEDURE_NAME": "SP_RECALC_STOCK", "PROCEDURE_ID": 1}]})
catalog = FirebirdCatalog(adapter)
procedures = catalog.get_procedures()
assert procedures == [{"PROCEDURE_NAME": "SP_RECALC_STOCK", "PROCEDURE_ID": 1}]
# a procedure name is never itself a relation, so it can never be
# threaded into sample_relation_rows
with pytest.raises(ValueError):
catalog.sample_relation_rows("SP_RECALC_STOCK", ["ID"], 1)
# --- read_only.py kept as an offline tool: bypass tests still apply ------
@pytest.mark.parametrize("payload", [
'"SOME_PROCEDURE"(1,2,3)',
'SELECT * FROM "SOME_PROC"(1)',
"SELECT * FROM PRODUCTS; DROP TABLE PRODUCTS",
"SELECT * FROM PRODUCTS -- comment",
"SELECT * FROM PRODUCTS /* comment */",
"EXECUTE PROCEDURE SP_DANGEROUS",
"SELECT SOME_UDF(1) FROM RDB$DATABASE",
"SELECT (SELECT PASSWORD FROM USERS) FROM PRODUCTS",
])
def test_offline_validator_still_rejects_every_bypass_payload(payload):
with pytest.raises(ValueError):
validate_read_only(payload)
@pytest.mark.parametrize("legit", [
"SELECT CAST(X AS INTEGER) FROM PRODUCTS",
"SELECT COUNT(*) FROM PRODUCTS",
"SELECT SUM(QTY), MIN(QTY), MAX(QTY), AVG(QTY) FROM STOCK GROUP BY PRODUCT_ID",
"SELECT COALESCE(X, 0) FROM PRODUCTS",
"WITH x AS (SELECT 1 AS n FROM RDB$DATABASE) SELECT n FROM x",
# UNION of two plain read-only SELECTs has no side effect -- the
# validator correctly accepts it rather than over-blocking.
"SELECT ID FROM A UNION SELECT ID FROM B",
# A quoted alias containing reserved-word-looking text or a semicolon
# is inert -- it can never execute anything, it is just a name; the
# masking correctly treats quoted content as opaque, not as SQL.
"SELECT * FROM PRODUCTS AS \"'; DROP TABLE X; --\"",
# A string literal containing reserved words is data, not SQL --
# masking must not accidentally treat it as a statement.
"SELECT 'reserved SELECT INSERT DELETE word inside a string' FROM PRODUCTS",
])
def test_offline_validator_accepts_legitimate_firebird_constructs(legit):
assert validate_read_only(legit) == legit
def test_offline_validator_is_not_wired_into_the_live_catalog_path():
"""Mission 00D: read_only.validate_read_only must not be imported by
catalog.py -- it is an optional offline tool, never the runtime gate."""
import gti_discovery.catalog as catalog_module
assert "validate_read_only" not in dir(catalog_module)
assert "read_only" not in catalog_module.__dict__