Commit 22b6a757 authored by ratara's avatar ratara Committed by oroulet

table names are now validated to prevent sql injection; invalid table names lead to an exception

parent b4b4f515
......@@ -24,3 +24,4 @@ examples/history.db
.eggs*
.coverage
schemas/UA-Nodese*
.vscode
# see https://www.sqlite.org/lang_keywords.html
sqlite3_keywords = [
"ABORT",
"ACTION",
"ADD",
"AFTER",
"ALL",
"ALTER",
"ALWAYS",
"ANALYZE",
"AND",
"AS",
"ASC",
"ATTACH",
"AUTOINCREMENT",
"BEFORE",
"BEGIN",
"BETWEEN",
"BY",
"CASCADE",
"CASE",
"CAST",
"CHECK",
"COLLATE",
"COLUMN",
"COMMIT",
"CONFLICT",
"CONSTRAINT",
"CREATE",
"CROSS",
"CURRENT",
"CURRENT_DATE",
"CURRENT_TIME",
"CURRENT_TIMESTAMP",
"DATABASE",
"DEFAULT",
"DEFERRABLE",
"DEFERRED",
"DELETE",
"DESC",
"DETACH",
"DISTINCT",
"DO",
"DROP",
"EACH",
"ELSE",
"END",
"ESCAPE",
"EXCEPT",
"EXCLUDE",
"EXCLUSIVE",
"EXISTS",
"EXPLAIN",
"FAIL",
"FILTER",
"FIRST",
"FOLLOWING",
"FOR",
"FOREIGN",
"FROM",
"FULL",
"GENERATED",
"GLOB",
"GROUP",
"GROUPS",
"HAVING",
"IF",
"IGNORE",
"IMMEDIATE",
"IN",
"INDEX",
"INDEXED",
"INITIALLY",
"INNER",
"INSERT",
"INSTEAD",
"INTERSECT",
"INTO",
"IS",
"ISNULL",
"JOIN",
"KEY",
"LAST",
"LEFT",
"LIKE",
"LIMIT",
"MATCH",
"MATERIALIZED",
"NATURAL",
"NO",
"NOT",
"NOTHING",
"NOTNULL",
"NULL",
"NULLS",
"OF",
"OFFSET",
"ON",
"OR",
"ORDER",
"OTHERS",
"OUTER",
"OVER",
"PARTITION",
"PLAN",
"PRAGMA",
"PRECEDING",
"PRIMARY",
"QUERY",
"RAISE",
"RANGE",
"RECURSIVE",
"REFERENCES",
"REGEXP",
"REINDEX",
"RELEASE",
"RENAME",
"REPLACE",
"RESTRICT",
"RETURNING",
"RIGHT",
"ROLLBACK",
"ROW",
"ROWS",
"SAVEPOINT",
"SELECT",
"SET",
"TABLE",
"TEMP",
"TEMPORARY",
"THEN",
"TIES",
"TO",
"TRANSACTION",
"TRIGGER",
"UNBOUNDED",
"UNION",
"UNIQUE",
"UPDATE",
"USING",
"VACUUM",
"VALUES",
"VIEW",
"VIRTUAL",
"WHEN",
"WHERE",
"WINDOW",
"WITH",
"WITHOUT"
]
class SqlInjectionError(Exception):
"""Raised, if a sql injection is detected."""
pass
def validate_table_name(table_name: str) -> None:
"""Checks wether the sql table name is valid or not."""
not_allowed_characters = [' ', ';', ',', '(', ')', '[', ']','"', "'"]
for character in table_name:
if character in not_allowed_characters:
raise SqlInjectionError(
f'table_name: {table_name} contains invalid character: {character}'
)
\ No newline at end of file
import logging
import aiosqlite
import sqlite3
from datetime import datetime, timedelta
from typing import Iterable
from datetime import timedelta
from datetime import datetime
import aiosqlite
from asyncua import ua
from ..ua.ua_binary import variant_from_binary, variant_to_binary
from ..common.utils import Buffer
from ..common.events import Event, get_event_properties_from_type_node
from ..common.sql_injection import validate_table_name
from ..common.utils import Buffer
from ..ua.ua_binary import variant_from_binary, variant_to_binary
from .history import HistoryStorageInterface
......@@ -41,6 +43,7 @@ class HistorySQLite(HistoryStorageInterface):
# create a table for the node which will store attributes of the DataValue object
# note: Value/VariantType TEXT is only for human reading, the actual data is stored in VariantBinary column
try:
validate_table_name(table)
await self._db.execute(
f'CREATE TABLE "{table}" (_Id INTEGER PRIMARY KEY NOT NULL,'
' ServerTimestamp TIMESTAMP,'
......@@ -57,6 +60,7 @@ class HistorySQLite(HistoryStorageInterface):
async def execute_sql_delete(self, condition: str, args: Iterable, table: str, node_id):
try:
validate_table_name(table)
await self._db.execute(f'DELETE FROM "{table}" WHERE {condition}', args)
await self._db.commit()
except aiosqlite.Error as e:
......@@ -66,6 +70,7 @@ class HistorySQLite(HistoryStorageInterface):
table = self._get_table_name(node_id)
# insert the data change into the database
try:
validate_table_name(table)
await self._db.execute(
f'INSERT INTO "{table}" VALUES (NULL, ?, ?, ?, ?, ?, ?)',
(
......@@ -85,9 +90,11 @@ class HistorySQLite(HistoryStorageInterface):
if period:
# after the insert, if a period was specified delete all records older than period
date_limit = datetime.utcnow() - period
validate_table_name(table)
await self.execute_sql_delete("SourceTimestamp < ?", (date_limit,), table, node_id)
if count:
# ensure that no more than count records are stored for the specified node
validate_table_name(table)
await self.execute_sql_delete(
'SourceTimestamp = (SELECT CASE WHEN COUNT(*) > ? '
f'THEN MIN(SourceTimestamp) ELSE NULL END FROM "{table}")',
......@@ -103,6 +110,7 @@ class HistorySQLite(HistoryStorageInterface):
results = []
# select values from the database; recreate UA Variant from binary
try:
validate_table_name(table)
async with self._db.execute(
f'SELECT * FROM "{table}" WHERE "SourceTimestamp" BETWEEN ? AND ? ' f'ORDER BY "_Id" {order} LIMIT ?',
(
......@@ -138,6 +146,7 @@ class HistorySQLite(HistoryStorageInterface):
# note that _Timestamp is for SQL query, _EventTypeName is for debugging, be careful not to create event
# properties with these names
try:
validate_table_name(table)
await self._db.execute(
f'CREATE TABLE "{table}" '
f'(_Id INTEGER PRIMARY KEY NOT NULL, _Timestamp TIMESTAMP, _EventTypeName TEXT, {columns})',
......@@ -153,6 +162,7 @@ class HistorySQLite(HistoryStorageInterface):
event_type = event.EventType # useful for troubleshooting database
# insert the event into the database
try:
validate_table_name(table)
await self._db.execute(
f'INSERT INTO "{table}" ("_Id", "_Timestamp", "_EventTypeName", {columns}) '
f'VALUES (NULL, "{event.Time}", "{event_type}", {placeholders})',
......@@ -167,6 +177,7 @@ class HistorySQLite(HistoryStorageInterface):
# after the insert, if a period was specified delete all records older than period
date_limit = datetime.utcnow() - period
try:
validate_table_name(table)
await self._db.execute(f'DELETE FROM "{table}" WHERE Time < ?', (date_limit.isoformat(' '),))
await self._db.commit()
except aiosqlite.Error as e:
......@@ -181,6 +192,7 @@ class HistorySQLite(HistoryStorageInterface):
results = []
# select events from the database; SQL select clause is built from EventFilter and available fields
try:
validate_table_name(table)
async with self._db.execute(
f'SELECT "_Timestamp", {clauses_str} FROM "{table}" '
f'WHERE "_Timestamp" BETWEEN ? AND ? ORDER BY "_Id" {order} LIMIT ?',
......
......@@ -7,22 +7,22 @@ same api on server and client side
"""
import asyncio
from datetime import datetime
from datetime import timedelta
import contextlib
import math
import tempfile
import os
import contextlib
import tempfile
from datetime import datetime, timedelta
import pytest
from asyncua import ua, uamethod, Node
from asyncua import Node, ua, uamethod
from asyncua.common import ua_utils
from asyncua.common.methods import call_method_full
from asyncua.common.copy_node_util import copy_node
from asyncua.common.instantiate_util import instantiate
from asyncua.common.structures104 import new_struct, new_enum, new_struct_field
from asyncua.ua.ua_binary import struct_to_binary, struct_from_binary
from asyncua.common.methods import call_method_full
from asyncua.common.sql_injection import validate_table_name, SqlInjectionError
from asyncua.common.structures104 import new_enum, new_struct, new_struct_field
from asyncua.ua.ua_binary import struct_from_binary, struct_to_binary
pytestmark = pytest.mark.asyncio
......@@ -1647,3 +1647,19 @@ async def test_custom_struct_with_strange_chars(opc):
var = await opc.opc.nodes.objects.add_variable(idx, "my_siemens_struct", ua.Variant(mystruct, ua.VariantType.ExtensionObject))
val = await var.read_value()
assert val.My_UInt32 == [78, 79]
async def test_sql_injection():
table = 'myTable'
validate_table_name(table)
table = 'my table'
with pytest.raises(SqlInjectionError) as _:
validate_table_name(table)
table = 'user;SELECT true'
with pytest.raises(SqlInjectionError) as _:
validate_table_name(table)
table = 'user"'
with pytest.raises(SqlInjectionError) as _:
validate_table_name(table)
table = "user'"
with pytest.raises(SqlInjectionError) as _:
validate_table_name(table)
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment