json2sql 1.0.6 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/json2sql/input_policy.rb +122 -0
- data/lib/json2sql/insert_model.rb +22 -10
- data/lib/json2sql/update_model.rb +18 -6
- data/lib/json2sql/version.rb +1 -1
- data/lib/json2sql.rb +1 -0
- metadata +4 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 20cb67d29822ef620acb57a06b9f5cb04c9e01139a83e264567fb595d6e02d1b
|
|
4
|
+
data.tar.gz: 6dbbf3faaa752c434a71a9f1a34d8fc4ca11aac7f104e7e7752d93b66357c902
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2117b66124364adedd912a28667416e2e09da573e050dde88f2c94eab66ceb0ec25ccec36d8c88a7f7e7e81332b42bf897f4ed9bc12ed60c2540a2c06ad17159
|
|
7
|
+
data.tar.gz: 96d7046e9c69b7dd66a4c6dbdd1b190dc805c230d450db60c6d3b8f4b634f0e5aac6995080e26a5e1d880ad15fa9b08e08828ee0b6141ba6102fb78502bbe846
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
module Json2sql
|
|
2
|
+
|
|
3
|
+
# Sanitizes a query input Hash before passing it to any Runner.
|
|
4
|
+
#
|
|
5
|
+
# Parameters:
|
|
6
|
+
# mode: :allow (default) — only tables listed in `tables:` are accessible.
|
|
7
|
+
# Tables absent from `tables:` are blocked entirely.
|
|
8
|
+
# Empty `tables:` = no restriction.
|
|
9
|
+
# :deny — all tables pass; only listed columns are stripped.
|
|
10
|
+
# tables: Per-table configuration Hash:
|
|
11
|
+
# { table_name => { columns: [...], where: { "and" => { col => val } } } }
|
|
12
|
+
# columns: column list — filtered by `mode` (allowed or denied).
|
|
13
|
+
# nil or absent = no column restriction for that table.
|
|
14
|
+
# where: server-side conditions merged into "and". Forced keys overwrite
|
|
15
|
+
# user-supplied values — primary IDOR guard.
|
|
16
|
+
#
|
|
17
|
+
# Usage:
|
|
18
|
+
# policy = Json2sql::InputPolicy.new(
|
|
19
|
+
# mode: :allow,
|
|
20
|
+
# tables: {
|
|
21
|
+
# orders: { columns: %w[id total status], where: { "and" => { "user_id" => 42 } } },
|
|
22
|
+
# users: { columns: %w[id name email] }
|
|
23
|
+
# }
|
|
24
|
+
# )
|
|
25
|
+
# safe_input = policy.apply(raw_params)
|
|
26
|
+
# sql = Json2sql::SelectRunner.build(safe_input)
|
|
27
|
+
|
|
28
|
+
class InputPolicy
|
|
29
|
+
|
|
30
|
+
def initialize(mode: :allow, tables: {})
|
|
31
|
+
|
|
32
|
+
@mode = mode
|
|
33
|
+
|
|
34
|
+
@tables = Json2sql.normalize(tables)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns a sanitized copy of input ready to pass to any Runner.
|
|
38
|
+
# Runners remain unmodified — they receive only the clean Hash.
|
|
39
|
+
|
|
40
|
+
def apply(input)
|
|
41
|
+
|
|
42
|
+
input = Json2sql.normalize(input)
|
|
43
|
+
|
|
44
|
+
input = filter_tables(input)
|
|
45
|
+
|
|
46
|
+
input.each { |table, params| sanitize_table(params, table) }
|
|
47
|
+
|
|
48
|
+
input
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def filter_tables(input)
|
|
54
|
+
|
|
55
|
+
return input if @mode == :deny || @tables.empty?
|
|
56
|
+
|
|
57
|
+
input.select { |table, _| @tables.key?(table) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def sanitize_table(params, table)
|
|
61
|
+
|
|
62
|
+
return unless params.is_a?(Hash)
|
|
63
|
+
|
|
64
|
+
filter_columns(params, table)
|
|
65
|
+
|
|
66
|
+
inject_where(params, table)
|
|
67
|
+
|
|
68
|
+
%w[children parents].each do |relation|
|
|
69
|
+
|
|
70
|
+
next unless params[relation].is_a?(Hash)
|
|
71
|
+
|
|
72
|
+
params[relation].each { |child_table, child_params| sanitize_table(child_params, child_table) }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Filters "columns" using mode (:allow or :deny).
|
|
77
|
+
# Handles Array (SELECT) and Hash (INSERT/UPDATE) column formats.
|
|
78
|
+
# Hash entries (function columns) always pass through in :allow mode.
|
|
79
|
+
# If no column list is defined for the table, columns are untouched.
|
|
80
|
+
|
|
81
|
+
def filter_columns(params, table)
|
|
82
|
+
|
|
83
|
+
columns = params["columns"]
|
|
84
|
+
|
|
85
|
+
return unless columns.is_a?(Array) || columns.is_a?(Hash)
|
|
86
|
+
|
|
87
|
+
list = @tables.dig(table, "columns")
|
|
88
|
+
|
|
89
|
+
return unless list.is_a?(Array)
|
|
90
|
+
|
|
91
|
+
params["columns"] = if @mode == :deny
|
|
92
|
+
|
|
93
|
+
columns.is_a?(Array) \
|
|
94
|
+
? columns.reject { |c| list.include?(c) } \
|
|
95
|
+
: columns.reject { |k, _| list.include?(k) }
|
|
96
|
+
|
|
97
|
+
else
|
|
98
|
+
|
|
99
|
+
columns.is_a?(Array) \
|
|
100
|
+
? columns.select { |c| c.is_a?(Hash) || list.include?(c) } \
|
|
101
|
+
: columns.select { |k, _| list.include?(k) }
|
|
102
|
+
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Merges forced "and" conditions into params["and"].
|
|
107
|
+
# Forced keys overwrite user-supplied values for the same column,
|
|
108
|
+
# preventing IDOR (e.g. attacker cannot override user_id).
|
|
109
|
+
|
|
110
|
+
def inject_where(params, table)
|
|
111
|
+
|
|
112
|
+
forced_and = @tables.dig(table, "where", "and")
|
|
113
|
+
|
|
114
|
+
return unless forced_and.is_a?(Hash)
|
|
115
|
+
|
|
116
|
+
params["and"] ||= {}
|
|
117
|
+
|
|
118
|
+
params["and"].merge!(forced_and)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -8,6 +8,8 @@ module Json2sql
|
|
|
8
8
|
# Values:
|
|
9
9
|
# Integer / Float → inserted as raw numbers
|
|
10
10
|
# String → wrapped in single quotes with SQL escaping
|
|
11
|
+
#
|
|
12
|
+
# Auto-injected when absent: created_at and updated_at → NOW()
|
|
11
13
|
|
|
12
14
|
class InsertModel
|
|
13
15
|
|
|
@@ -20,28 +22,41 @@ module Json2sql
|
|
|
20
22
|
|
|
21
23
|
def build(params)
|
|
22
24
|
|
|
25
|
+
columns = params["columns"]
|
|
26
|
+
|
|
27
|
+
return unless columns.is_a?(Hash)
|
|
28
|
+
|
|
29
|
+
columns = build_timestamps(columns)
|
|
30
|
+
|
|
23
31
|
@sql << "INSERT INTO "
|
|
24
32
|
|
|
25
33
|
@sql << Sanitizer.keyword_wrap(@table)
|
|
26
34
|
|
|
27
35
|
@sql << " ("
|
|
28
36
|
|
|
29
|
-
build_columns(
|
|
37
|
+
build_columns(columns)
|
|
30
38
|
|
|
31
39
|
@sql << ") VALUES ("
|
|
32
40
|
|
|
33
|
-
build_values(
|
|
41
|
+
build_values(columns)
|
|
34
42
|
|
|
35
43
|
@sql << ")"
|
|
36
44
|
end
|
|
37
45
|
|
|
38
46
|
private
|
|
39
47
|
|
|
40
|
-
def
|
|
48
|
+
def build_timestamps(columns)
|
|
41
49
|
|
|
42
|
-
|
|
50
|
+
timestamps = {}
|
|
43
51
|
|
|
44
|
-
|
|
52
|
+
timestamps["created_at"] = :now unless columns.key?("created_at")
|
|
53
|
+
|
|
54
|
+
timestamps["updated_at"] = :now unless columns.key?("updated_at")
|
|
55
|
+
|
|
56
|
+
timestamps.empty? ? columns : columns.merge(timestamps)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def build_columns(columns)
|
|
45
60
|
|
|
46
61
|
separator = false
|
|
47
62
|
|
|
@@ -55,11 +70,7 @@ module Json2sql
|
|
|
55
70
|
end
|
|
56
71
|
end
|
|
57
72
|
|
|
58
|
-
def build_values(
|
|
59
|
-
|
|
60
|
-
columns = params["columns"]
|
|
61
|
-
|
|
62
|
-
return unless columns.is_a?(Hash)
|
|
73
|
+
def build_values(columns)
|
|
63
74
|
|
|
64
75
|
separator = false
|
|
65
76
|
|
|
@@ -73,6 +84,7 @@ module Json2sql
|
|
|
73
84
|
when Float then @sql << value.to_s
|
|
74
85
|
when Integer then @sql << value.to_s
|
|
75
86
|
when String then @sql << Sanitizer.value_wrap(value)
|
|
87
|
+
when :now then @sql << "NOW()"
|
|
76
88
|
end
|
|
77
89
|
end
|
|
78
90
|
end
|
|
@@ -8,6 +8,8 @@ module Json2sql
|
|
|
8
8
|
# "or" => { ... } – WHERE conditions (OR)
|
|
9
9
|
#
|
|
10
10
|
# Value types follow the same rules as InsertModel.
|
|
11
|
+
#
|
|
12
|
+
# Auto-injected when absent: updated_at → NOW()
|
|
11
13
|
|
|
12
14
|
class UpdateModel
|
|
13
15
|
|
|
@@ -22,31 +24,40 @@ module Json2sql
|
|
|
22
24
|
|
|
23
25
|
def build(params)
|
|
24
26
|
|
|
27
|
+
columns = params["columns"]
|
|
28
|
+
|
|
29
|
+
return unless columns.is_a?(Hash)
|
|
30
|
+
|
|
31
|
+
columns = build_timestamp(columns)
|
|
32
|
+
|
|
25
33
|
@sql << "UPDATE "
|
|
26
34
|
|
|
27
35
|
@sql << Sanitizer.keyword_wrap(@table)
|
|
28
36
|
|
|
29
37
|
@sql << " SET "
|
|
30
|
-
|
|
31
|
-
build_columns(
|
|
38
|
+
|
|
39
|
+
build_columns(columns)
|
|
32
40
|
|
|
33
41
|
WhereModel.new(@sql, @table, @relation).build(params)
|
|
34
42
|
end
|
|
35
43
|
|
|
36
44
|
private
|
|
37
45
|
|
|
38
|
-
def
|
|
46
|
+
def build_timestamp(columns)
|
|
39
47
|
|
|
40
|
-
columns
|
|
48
|
+
return columns if columns.key?("updated_at")
|
|
41
49
|
|
|
42
|
-
|
|
50
|
+
columns.merge("updated_at" => :now)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def build_columns(columns)
|
|
43
54
|
|
|
44
55
|
separator = false
|
|
45
56
|
|
|
46
57
|
columns.each do |key, value|
|
|
47
58
|
|
|
48
59
|
@sql << ", " if separator
|
|
49
|
-
|
|
60
|
+
|
|
50
61
|
separator = true
|
|
51
62
|
|
|
52
63
|
column = key.to_s
|
|
@@ -61,6 +72,7 @@ module Json2sql
|
|
|
61
72
|
when Float then @sql << value.to_s
|
|
62
73
|
when Integer then @sql << value.to_s
|
|
63
74
|
when String then @sql << Sanitizer.value_wrap(value)
|
|
75
|
+
when :now then @sql << "NOW()"
|
|
64
76
|
end
|
|
65
77
|
end
|
|
66
78
|
end
|
data/lib/json2sql/version.rb
CHANGED
data/lib/json2sql.rb
CHANGED
|
@@ -10,6 +10,7 @@ require_relative "json2sql/update_model"
|
|
|
10
10
|
require_relative "json2sql/update_runner"
|
|
11
11
|
require_relative "json2sql/delete_model"
|
|
12
12
|
require_relative "json2sql/delete_runner"
|
|
13
|
+
require_relative "json2sql/input_policy"
|
|
13
14
|
|
|
14
15
|
# Json2sql — SQL builder that generates MySQL/MariaDB query strings from
|
|
15
16
|
# plain Ruby Hashes (or parsed JSON).
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: json2sql
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tiago da Silva
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: minitest
|
|
@@ -50,6 +49,7 @@ files:
|
|
|
50
49
|
- lib/json2sql.rb
|
|
51
50
|
- lib/json2sql/delete_model.rb
|
|
52
51
|
- lib/json2sql/delete_runner.rb
|
|
52
|
+
- lib/json2sql/input_policy.rb
|
|
53
53
|
- lib/json2sql/insert_model.rb
|
|
54
54
|
- lib/json2sql/insert_runner.rb
|
|
55
55
|
- lib/json2sql/sanitizer.rb
|
|
@@ -66,7 +66,6 @@ licenses:
|
|
|
66
66
|
metadata:
|
|
67
67
|
homepage_uri: https://github.com/tyagoy/json2sql
|
|
68
68
|
source_code_uri: https://github.com/tyagoy/json2sql
|
|
69
|
-
post_install_message:
|
|
70
69
|
rdoc_options: []
|
|
71
70
|
require_paths:
|
|
72
71
|
- lib
|
|
@@ -81,8 +80,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
81
80
|
- !ruby/object:Gem::Version
|
|
82
81
|
version: '0'
|
|
83
82
|
requirements: []
|
|
84
|
-
rubygems_version: 3.
|
|
85
|
-
signing_key:
|
|
83
|
+
rubygems_version: 3.6.7
|
|
86
84
|
specification_version: 4
|
|
87
85
|
summary: Translates Ruby Hashes (or parsed JSON) into MySQL/MariaDB query strings.
|
|
88
86
|
test_files: []
|