Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
O
opcua-asyncio
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Nikola Balog
opcua-asyncio
Commits
22b6a757
Commit
22b6a757
authored
Jan 22, 2023
by
ratara
Committed by
oroulet
Jan 22, 2023
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
table names are now validated to prevent sql injection; invalid table names lead to an exception
parent
b4b4f515
Changes
4
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
206 additions
and
13 deletions
+206
-13
.gitignore
.gitignore
+1
-0
asyncua/common/sql_injection.py
asyncua/common/sql_injection.py
+164
-0
asyncua/server/history_sql.py
asyncua/server/history_sql.py
+17
-5
tests/test_common.py
tests/test_common.py
+24
-8
No files found.
.gitignore
View file @
22b6a757
...
...
@@ -24,3 +24,4 @@ examples/history.db
.eggs*
.coverage
schemas/UA-Nodese*
.vscode
asyncua/common/sql_injection.py
0 → 100644
View file @
22b6a757
# 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
asyncua/server/history_sql.py
View file @
22b6a757
import
logging
import
aiosqlite
import
sqlite3
from
datetime
import
datetime
,
timedelta
from
typing
import
Iterable
from
datetime
import
timedelta
from
datetime
import
datetim
e
import
aiosqlit
e
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 ?'
,
...
...
tests/test_common.py
View file @
22b6a757
...
...
@@ -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
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment