Skip to content

Commit dce2e52

Browse files
author
PingoLee
committed
Implement SQL Injection Prevention Measures
- Introduced a SQLSanitizer module for sanitizing identifiers and escaping LIKE patterns. - Added parameterized query support to prevent SQL injection vulnerabilities. - Updated filter query generation to utilize parameterized queries and sanitize user inputs. - Enhanced the execution layer to support parameterized query execution for both PostgreSQL and SQLite. - Added validation functions for field names and operators to ensure safe query construction. - Implemented comprehensive tests for SQL injection prevention, including basic parameter injection, field name validation, and LIKE pattern escaping.
1 parent 4ef46bf commit dce2e52

10 files changed

Lines changed: 991 additions & 220 deletions

File tree

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
88
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
99
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
1010
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
11+
Decimals = "abce61dc-4473-55a0-ba07-351d65e31d42"
1112
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
1213
Infiltrator = "5903a43b-9cc3-4c30-8d17-598619ec4e9b"
1314
Inflector = "6d011eab-0732-4556-8808-e463c76bf3b6"
@@ -27,6 +28,7 @@ XLSX = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0"
2728
YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6"
2829

2930
[compat]
31+
Decimals = "0.4.1"
3032
Infiltrator = "1.8.3"
3133
OrderedCollections = "1.7.0"
3234
Printf = "1.11.0"

src/Configuration.jl

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module Configuration
22

33
import YAML, Logging
4-
import PormG: SQLConn, PormGPostgres, PormGSQLite, config
4+
import PormG: SQLConn, PormGPostgres, PormGPostgresParam, PormGSQLite, config
55
import PormG: PORMG_DB_CONFIG_FILE_NAME, DB_PATH, MODEL_FILE, DATETIME_FORMAT
66
import PormG: Generator
77
import PormG.Infiltrator: @infiltrate
@@ -375,30 +375,46 @@ function is_connection_error(e, connection::PormGPostgres)
375375
# occursin("connection timeout", msg)
376376
end
377377

378-
function fetch(connection::PormGPostgres, sql::String; conn::Union{Nothing, LibPQ.Connection} = nothing)
378+
function libpq_execute(conn::LibPQ.Connection, sql::String, params::Nothing)
379+
return LibPQ.execute(conn, sql)
380+
end
381+
function libpq_execute(conn::LibPQ.Connection, sql::String, params::Vector{Any})
382+
return LibPQ.execute(conn, sql, params)
383+
end
384+
libpq_execute(conn::LibPQ.Connection, sql::String, params::PormGPostgresParam) = libpq_execute(conn, sql, params.parameters)
385+
386+
function fetch(connection::PormGPostgres, sql::String;
387+
conn::Union{Nothing, LibPQ.Connection} = nothing,
388+
params::Union{Nothing, PormGPostgresParam} = nothing)
379389
@infiltrate false
380390
conn === nothing && (conn = acquire_connection(connection))
381391
try
382-
return LibPQ.execute(conn, sql)
392+
return libpq_execute(conn, sql, params)
383393
catch e
384394
@infiltrate
385395
if is_connection_error(e, connection)
386396
@warn "Lost connection to database. Attempting to reconnect..."
387397
conn = reconnect_db(connection, conn)
388-
return LibPQ.execute(conn, sql)
398+
return libpq_execute(conn, sql, params)
389399
end
390400
@error "Failed to execute SQL query: $e"
391401
throw(e)
392402
finally
393403
release_connection(connection, conn)
394404
end
395405
end
396-
fetch(settings::SQLConn, sql::String; conn::Union{Nothing, LibPQ.Connection} = nothing) = fetch(settings.connections, sql; conn=conn)
397-
398-
function with_transaction(pool::PormGPostgres, sql::String; conn::Union{Nothing, LibPQ.Connection} = nothing, release_conn::Bool = false)
406+
fetch(settings::SQLConn, sql::String; conn::Union{Nothing, LibPQ.Connection} = nothing, params::Union{Nothing, PormGPostgresParam} = nothing) = fetch(settings.connections, sql; conn=conn, params=params)
407+
fetch(settings::SQLConn, sql::String, params::PormGPostgresParam; conn::Union{Nothing, LibPQ.Connection} = nothing) = fetch(settings.connections, sql; conn=conn, params=params)
408+
fetch(settings::PormGPostgres, sql::String, params::PormGPostgresParam; conn::Union{Nothing, LibPQ.Connection} = nothing) = fetch(settings, sql; conn=conn, params=params)
409+
410+
function with_transaction(pool::PormGPostgres, sql::String;
411+
conn::Union{Nothing, LibPQ.Connection} = nothing,
412+
release_conn::Bool = false,
413+
params::Union{Nothing, PormGPostgresParam} = nothing)
414+
399415
conn === nothing && (conn = acquire_connection(pool))
400416
try
401-
return LibPQ.execute(conn, sql), conn
417+
return libpq_execute(conn, sql, params), conn
402418
catch e
403419
@infiltrate
404420
@error "Failed to execute SQL transaction, rolling back: $e"
@@ -409,7 +425,8 @@ function with_transaction(pool::PormGPostgres, sql::String; conn::Union{Nothing,
409425
end
410426
end
411427
end
412-
with_transaction(pool::SQLConn, sql::String; conn::Union{Nothing, LibPQ.Connection} = nothing, release_conn::Bool = false) = with_transaction(pool.connections, sql; conn=conn, release_conn=release_conn)
428+
with_transaction(pool::SQLConn, sql::String; conn::Union{Nothing, LibPQ.Connection} = nothing, release_conn::Bool = false, params::Union{Nothing, PormGPostgresParam} = nothing) = with_transaction(pool.connections, sql; conn=conn, release_conn=release_conn, params=params)
429+
413430

414431

415432

src/Dialect.jl

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -464,17 +464,15 @@ end
464464
# Function to deal with operators
465465
#
466466

467-
function contains(conn::PormGPostgres, column::String, value::String)::String
468-
value = replace(value, "'" => "")
469-
return """$(column) LIKE '%$(value)%'"""
467+
function contains(conn::PormGPostgres, column::String, value::String)::String
468+
return "$(column) LIKE $(value)"
470469
end
471470
function contains(conn::PormGPostgres, column::String, value)
472471
throw(ArgumentError("The value must be a String"))
473472
return nothing
474473
end
475474
function icontains(conn::PormGPostgres, column::String, value::String)::String
476-
value = replace(value, "'" => "")
477-
return """$(column) ILIKE '%$(value)%'"""
475+
return "$(column) ILIKE $(value)"
478476
end
479477
function icontains(conn::PormGPostgres, column::String, value)
480478
throw(ArgumentError("The value must be a String"))

src/Generator.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ function create_db_folder_and_yml(;path::String = DB_PATH)::Nothing
6161
nothing
6262
end
6363

64-
function generate_models_from_db(file::String, Instructions::Vector{Any}, settings::SQLConn) :: Nothing
64+
function generate_models_from_db(file::String, Instructions::Vector{Any}, settings::SQLConn; path::String = MODEL_PATH) :: Nothing
6565

66-
open(joinpath(MODEL_PATH, file), "w") do f
66+
open(joinpath(path, file), "w") do f
6767
write(f, """module $(basename(file) |> x -> replace(x, ".jl" => ""))\n
6868
import PormG.Models
6969
import PormG.Models: RESTRICT, CASCADE, SET_NULL, SET_DEFAULT, DO_NOTHING

src/Migrations.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1203,7 +1203,7 @@ function import_models_from_django(
12031203

12041204
end
12051205

1206-
generate_models_from_db(file, Instructions, settings)
1206+
generate_models_from_db(file, Instructions, settings, path=db)
12071207
end
12081208

12091209
function parse_class(model_py_string::String)

src/Models.jl

Lines changed: 66 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import PormG: SQLConn, config, Configuration
77
import PormG: CASCADE, RESTRICT, SET_NULL, SET_DEFAULT, SET, DO_NOTHING, PROTECT
88
using Printf
99
import Base.deepcopy
10+
using Decimals
11+
1012

1113
import PormG.Infiltrator: @infiltrate
1214

@@ -331,35 +333,40 @@ end
331333
#
332334

333335
function format_text_sql(value::Union{Int, Date, DateTime, ZonedDateTime})
334-
return string("'", value, "'")
336+
return string(value)
335337
end
336338
function format_text_sql(value::Union{Missing, Nothing})
337-
return "null"
339+
return missing
338340
end
339341
function format_text_sql(value::Bool)
340-
return value ? "'true'" : "'false'"
342+
return value
343+
# return value ? "'true'" : "'false'"
341344
end
342345
function format_text_sql(value::AbstractString)
343-
return string("'", replace(value, "'" => "`"), "'")
346+
return value
347+
# return string("'", replace(value, "'" => "`"), "'")
344348
end
345349
function format_text_sql(value::AbstractArray)
346350
arrayref::Vector{String} = []
347351
for v in value
348352
push!(arrayref, v |> format_text_sql)
349353
end
350354
@infiltrate false
351-
return string("(", join(arrayref, ","), ")")
355+
# return string("(", join(arrayref, ","), ")")
356+
return arrayref
352357
end
353358

354359
function format_number_sql(value::Integer)
355-
return string(value)
360+
return value
361+
# return string(value)
356362
end
357363
function format_number_sql(value::Union{Missing, Nothing})
358-
return "null"
364+
return missing
359365
end
360366
function format_number_sql(value::Union{Float16, Float32, Float64})
361367
# Use @sprintf to avoid scientific notation and ensure full precision
362-
return string("'", @sprintf("%.17g", value), "'")
368+
# return string("'", @sprintf("%.17g", value), "'")
369+
return @sprintf("%.17g", value)
363370
end
364371
function format_number_sql(value::AbstractString)
365372
# try integer first
@@ -376,58 +383,71 @@ function format_number_sql(value::AbstractString)
376383
end
377384
end
378385
function format_number_sql(value::AbstractArray)
379-
arrayref::Vector{String} = []
386+
arrayref::Vector{Union{String, Integer, Missing}} = []
380387
for v in value
381388
push!(arrayref, v |> format_number_sql)
382389
end
383-
return string("(", join(arrayref, ","), ")")
390+
# return string("(", join(arrayref, ","), ")")
391+
return arrayref
392+
end
393+
function format_number_sql(value::Decimals.Decimal)
394+
try
395+
return string(value)
396+
catch e
397+
@error("Failed to format Decimals.Decimal value: $(e)", value=value)
398+
throw(e)
399+
end
384400
end
385401

386402
function format_bool_sql(value::Integer)
387403
if value in [0, 1] == false
388404
throw(ArgumentError("The value must be 0, 1, true or false"))
389405
end
390-
return value == 1 ? "true" : "false"
406+
return value == 1 ? true : false
391407
end
392408
function format_bool_sql(value::Union{Missing, Nothing})
393-
return "null"
409+
return missing
394410
end
395411
function format_bool_sql(value::Bool)
396-
return value ? "true" : "false"
412+
return value
397413
end
398414

399415
function format_date_sql(value::Date)
400-
return string("'", value, "'")
416+
# return string("'", value, "'")
417+
return value |> string
401418
end
402419
function format_date_sql(value::Union{Missing, Nothing})
403-
return "null"
420+
return missing
404421
end
405422
function format_date_sql(value::DateTime)
406-
return string("'", value |> Dates.Date, "'")
423+
# return string("'", value |> Dates.Date, "'")
424+
return value |> Dates.Date |> string
407425
end
408426
function format_date_sql(value::ZonedDateTime)
409-
return string("'", value |> Dates.Date, "'")
427+
# return string("'", value |> Dates.Date, "'")
428+
return value |> Dates.Date |> string
410429
end
411430
function format_date_sql(value::AbstractString)
412431
if occursin(r"^\d{4}-\d{2}-\d{2}$", value)
413-
return string("'", value, "'")
432+
return value
414433
else
415434
throw(ArgumentError("The date $value is invalid"))
416435
end
417436
end
418-
function format_date_sql()
437+
function format_date_sql(value)
419438
throw(ArgumentError("The date must be a Date, DateTime, ZonedDateTime or a string in the format YYYY-MM-DD"))
420439
end
421440

422441

423442
function format_timezone_sql(value::String; format::String=DATETIME_FORMAT)
424-
return validate_timezone(value, format) ? string("'", value, "'") : throw(ArgumentError("The timezone $value is invalid"))
443+
return validate_timezone(value, format) ? string(value) : throw(ArgumentError("The timezone $value is invalid"))
425444
end
426445
function format_timezone_sql(value::Union{Missing, Nothing})
427-
return "null"
446+
return missing
428447
end
429448
function format_timezone_sql(value::ZonedDateTime)
430-
return string("'", value, "'")
449+
# return string("'", value, "'")
450+
return value |> string
431451
end
432452
function format_timezone_sql(value::DateTime, timezone::String)
433453
# function used just in create
@@ -436,7 +456,7 @@ end
436456

437457
function format_yyyy_mm(value::String)
438458
if occursin(r"^\d{4}-\d{2}$", value)
439-
return string("'", value, "'")
459+
return value
440460
else
441461
throw(ArgumentError("The value $value is invalid, it must be in the format YYYY-MM"))
442462
end
@@ -445,7 +465,7 @@ function format_yyyy_mm(value::Integer)
445465
value = string(value)
446466
if length(value) == 6
447467
# Format as YYYY-MM
448-
return string("'", value[1:4], "-", value[5:6], "'")
468+
return string(value[1:4], "-", value[5:6])
449469
else
450470
throw(ArgumentError("The value $value must be a 6-digit integer in the format YYYYMM or a string in the format YYYY-MM"))
451471
end
@@ -1026,7 +1046,28 @@ end
10261046
formater::Function = format_date_sql
10271047
end
10281048

1029-
function DateField(; verbose_name=nothing, unique=false, blank=false, null=false, db_index=false, default=nothing, editable=false, auto_now=false, auto_now_add=false)
1049+
function DateField(; kwargs...)
1050+
# List of accepted parameters
1051+
accepted = Set([
1052+
:verbose_name, :unique, :blank, :null, :db_index, :default, :editable, :auto_now, :auto_now_add
1053+
])
1054+
# Check for unexpected parameters
1055+
for (k, v) in kwargs
1056+
if !(k in accepted)
1057+
@warn "Unexpected parameter for DateField. It will be ignored." field="DateField" param=k value=v
1058+
end
1059+
end
1060+
# Extract parameters with defaults
1061+
verbose_name = get(kwargs, :verbose_name, nothing)
1062+
unique = get(kwargs, :unique, false)
1063+
blank = get(kwargs, :blank, false)
1064+
null = get(kwargs, :null, false)
1065+
db_index = get(kwargs, :db_index, false)
1066+
default = get(kwargs, :default, nothing)
1067+
editable = get(kwargs, :editable, false)
1068+
auto_now = get(kwargs, :auto_now, false)
1069+
auto_now_add = get(kwargs, :auto_now_add, false)
1070+
10301071
# Validate verbose_name
10311072
!(verbose_name isa Union{Nothing, String}) && throw(ArgumentError("The verbose_name must be a String or nothing"))
10321073
# Validate default

src/PormG.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ using LibPQ
1515
abstract type PormGAbstractType end
1616
abstract type SQLConn <: PormGAbstractType end
1717
abstract type PormGPostgres <: SQLConn end
18+
abstract type PormGPostgresParam <: PormGPostgres end
1819
abstract type PormGSQLite <: SQLConn end
1920
abstract type SQLObject <: PormGAbstractType end
2021
abstract type SQLObjectHandler <: SQLObject end

0 commit comments

Comments
 (0)