Rails sanitize user input in active record query - sql

Let's say I have following data:
models/supplier.rb
| -- | ---------------- |
| id | name |
| -- | ---------------- |
| 1 | John Doe's Store |
| 2 | Jane |
| -- | ---------------- |
I have following queries which are failing to sanitize user's input from search field:
#term = "John Doe's"
Query 1
Supplier.order("case when name LIKE :term 1 else 2 end, name asc", term: "#{#term}%")
ArgumentError: Direction "one's%" is invalid. Valid directions are: [:asc, :desc, :ASC, :DESC, "asc", "desc", "ASC", "DESC"]
from ~/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activerecord-4.2.7/lib/active_record/relation/query_methods.rb:1113:in `block (2 levels) in validate_order_args'
Query 2
Vulnerable to SQL Injection attack
Supplier.order("case when name LIKE '#{#term}%' then 1 else 2 end, name ASC").first
Supplier Load (2.6ms) SELECT "suppliers".* FROM "suppliers" ORDER BY case when name LIKE 'John Doe's%' then 1 else 2 end LIMIT 1
ActiveRecord::StatementInvalid: PG::SyntaxError: ERROR: syntax error at or near "s"
LINE 1: ...uppliers" ORDER BY case when name LIKE 'John Doe's%' then 1..
Query 3
It will success for normal inputs which don't have ' (special character) in them, but this query is still vulnerable to SQL injection attack
#term = "John"
Supplier.order("case when name LIKE '#{#term}%' then 1 else 2 end, name ASC").first
#<Supplier:0x007fe4bfd8d758
id: 188,
name: "John Doe's Store">
I am not able to figure out a solution for this problem, Please help me complete this query in secure way.

All you need to escape your input is to use
ActiveRecord::Base.connection.quote(value)
This works for all types and is what rails use.
#term = ActiveRecord::Base.connection.quote("John Doe's" + "%" )
Supplier.order("case when name LIKE #{#term} then 1 else 2 end, name ASC").first

You can use the base connection quote which will sanitize the input
#term = "John Doe's"
like_value = ActiveRecord::Base.connection.quote(#term + '%')
Supplier.order("case when name LIKE #{like_value} 1 else 2 end, name asc")
Read about it here...
http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Quoting.html#method-i-quote

Rails offers sanitize_sql_like() for this.
So your example would look like this:
#term = "John Doe's"
like_value = sanitize_sql_like(#term + '%')
Supplier.order("case when name LIKE #{like_value} 1 else 2 end, name asc")
See:
https://api.rubyonrails.org/classes/ActiveRecord/Sanitization/ClassMethods.html#method-i-sanitize_sql_like
EDIT:
If you need to use it in a Controller, call it like this:
ActiveRecord::Base::sanitize_sql_like()

Related

Postgres full text search error when using setweight()

I have PostgreSQL 9.6.8 running on Fedora 27 64bit. When i execute this query:
UPDATE tbl SET textsearchable_index_col =
setweight(to_tsvector('french', coalesce("col1",'')), 'D') ||
setweight(to_tsvector('french', coalesce("col2",'')), 'D');
I get this error:
ERROR: cache lookup failed for function 3625
********** Error **********
ERROR: cache lookup failed for function 3625
SQL state: XX000
but when I execute either:
UPDATE tbl SET textsearchable_index_col =
setweight(to_tsvector('french', coalesce("col1",'')), 'D');
or
UPDATE tbl SET textsearchable_index_col =
setweight(to_tsvector('french', coalesce("col2",'')), 'D');
I get:
Query returned successfully: 0 rows affected, 11 msec execution time.
My question is why does it work for either column individually but it does not work when together?
This link shows that it should be possible to use both columns in the same query (at the end of section 12.3.1).
Edit: here is what the system returns for Laurenz's queries. The first query returns
oprname | oprleft | oprright | oprcode
---------+----------+----------+----------
|| | tsvector | tsvector | 3625
The second query returns an empty result set.
Your database is corrupted, and you are lacking the function tsvector_concat which is the function behind the || operator.
This is how it should look on a healthy system:
SELECT oprname, oprleft::regtype, oprright::regtype, oprcode
FROM pg_operator
WHERE oid = 3633;
oprname | oprleft | oprright | oprcode
---------+----------+----------+-----------------
|| | tsvector | tsvector | tsvector_concat
(1 row)
SELECT proname, proargtypes::regtype[], prosrc
FROM pg_proc
WHERE oid = 3625;
proname | proargtypes | prosrc
-----------------+---------------------------+-----------------
tsvector_concat | [0:1]={tsvector,tsvector} | tsvector_concat
(1 row)
The second part is missing in your case.
You should restore from a backup.
Try to figure out how you got into this mess so that you can avoid it in the future.

Removing varchar(s) after a semicolon

So I have a lot of data like this:
pix11co;10.115.0.1
devapp087co;10.115.0.100
old_main-mgr;10.115.0.101
radius03co;10.115.0.110
And I want to delete the stuff after the ; so it just becomes
pix11co
devapp087co
old_main-mgr
radius03co
Since they're all different I can live with the semi-colon staying there.
I have the following query and it runs successfully but doesn't delete anything.
UPDATE dns$ SET [Name;] = REPLACE ([Name;], '%_;%__________%', '%_;');
What wildcards can I use to specify the characters after the ; ?
Can you use CHARINDEX? E.g.:
SELECT LEFT('pix11co;10.115.0.1', CHARINDEX(';', 'pix11co;10.115.0.1') - 1)
You can use SUBSTRING() and CHARINDEX() functions:
CREATE TABLE MyStrings (
STR VARCHAR(MAX)
);
INSERT INTO MyStrings VALUES
('pix11co;10.115.0.1'),
('devapp087co;10.115.0.100'),
('old_main-mgr;10.115.0.101'),
('radius03co;10.115.0.110');
SELECT STR, SUBSTRING(STR, 1, CHARINDEX(';', STR) -1 ) AS Result
FROM MyStrings;
Results:
+---------------------------+--------------+
| STR | Result |
+---------------------------+--------------+
| pix11co;10.115.0.1 | pix11co |
| devapp087co;10.115.0.100 | devapp087co |
| old_main-mgr;10.115.0.101 | old_main-mgr |
| radius03co;10.115.0.110 | radius03co |
+---------------------------+--------------+

Update Unique Column partial string SQL

I have a table that has a user names in the column. This is an example
| **Path**
| /test/path/Barry/home
| /test/path/Jenny/home
| /test/path/Jermehiam/home/Docs
| /test/path/Sarah/home/Docs
I am not sure how to update just the part of the path that ends at 'home'. I need the other parts of the path to remain as the string I am replacing is with an environment variable. So the end result would be:
| **Path**
| ${PATH}
| ${PATH}
| ${PATH}/Docs
| ${PATH}/Docs
Any help would be appreciated
You may do it in this way:
UPDATE PathTable
SET Path = '...'
WHERE Path LIKE '/test/path/%/home'
UPDATE PathTable
SET Path = '.../Docs'
WHERE Path LIKE '/test/path/%/home/Docs'
if you are using SQL-Server you can use CHARINDEX(), SUBSTRING() and LEN()
SELECT CASE WHEN CHARINDEX('home',url) = 0
THEN url
ELSE '${PATH}' + SUBSTRING(url, (CHARINDEX('home',url) + 4), LEN(url) - (CHARINDEX('home',url) + 3))
END url
FROM t_link
in Oracle, you can use INSTR(), SUBSTR() and LENGTH()
SELECT CASE WHEN CHARINDEX('home',url) = 0
THEN url
ELSE '${PATH}' + SUBSTRING(url, (CHARINDEX('home',url) + 4), LEN(url) - (CHARINDEX('home',url) + 3))
END url
FROM t_link
Result
url
**Path**
${PATH}
${PATH}
${PATH}/Docs
${PATH}/Docs

Replace empty/null string with result from another record

I have a language table and want retrieve specific records for a selected language. However, when there is no translation present I want to get the translation of another language.
TRANSLATIONS
TAG LANG TEXT
"prog1" | 1 | "Programmeur"
"prog1" | 2 | "Programmer"
"prog1" | 3 | "Programista"
"prog2" | 1 | ""
"prog2" | 2 | "Category"
"prog2" | 3 | "Kategoria"
"prog3" | 1 | "Actie"
"prog3" | 2 | "Action"
"prog3" | 3 | "Dzialanie"
PROGDATA
ID | COL1 | COL2
1 | "data" | "data"
2 | "data" | "data"
3 | "data" | "data"
If I want translations from language 3 based on the ID's in table PROGDATA then I can do:
SELECT TEXT FROM TRANSLATIONS, PROGDATA
WHERE TRANSLATIONS.TAG="prog" & PROGDATA.ID
AND TRANSLATIONS.LANG=3
which would give me:
"Programista"
"Kategoria"
"Dzialanie"
In case of language 1 I get an empty string on the second record:
"Programmeur"
""
"Actie"
How can I replace the empty string with, for example, the translation of language 2?
"Programmeur"
"Category"
"Actie"
I tried nesting a new select query in an IIf() function but that obviously did not work.
SELECT
IIf(TEXT="",
(SELECT TEXT FROM TRANSLATIONS, PROGDATA
WHERE TRANSLATIONS.TAG="prog" & PROGDATA.ID
AND TRANSLATIONS.LANG=2),TEXT)
FROM TRANSLATIONS, PROGDATA
WHERE TRANSLATIONS.TAG="prog" & PROGDATA.ID
AND TRANSLATIONS.LANG=3
A SWITCH or CASE statement may work well. But try this:
SELECT
IIf(TEXT="",
(SELECT TEXT AS TEXT_OTHER FROM TRANSLATIONS, PROGDATA
WHERE TRANSLATIONS.TAG="prog" & PROGDATA.ID
AND TRANSLATIONS.LANG=2),TEXT) AS TEXT_FINAL
I am using TEXTOTHER and TEXTFINAL to reduce ambiguity in your field names. Sometimes this helps.
You may even need to apply the principle to the table name:
(SELECT TEXT AS TEXT_OTHER FROM TRANSLATIONS AS TRANSLATIONS_ALT...
Also, make sure your criterion is correct: an empty string, not a Null value.
IIf(TEXT="", ...
IIf(ISNULL(TEXT), ...
You can join TRANSLATIONS table again to get a "default" translation and use a CASE in the SELECT Statement.
SELECT
CASE
WHEN ISNULL(Translation.TEXT,"") = "" THEN DefaultLang.TEXT
ELSE Translation.Text
END
FROM TRANSLATIONS AS DefaultLang,TRANSLATIONS as Translation, PROGDATA
WHERE
DefaultLang.TAG="prog" & PROGDATA.ID AND Translation.TAG="prog" & PROGDATA.ID
AND DefaultLang.LANG=2
AND Translation.LANG=3
it is a pseudo-code idea...
I d try to add a checkEmpty function for each value returned. if is not empty, return the same.. if is empty return a new search from other languaje.
You need to cheak that the new value is not empty again of course.
create function checkEmpty( #word varchar(10), #languageNumber integer) returns varchar(10)
as
begin
declare #newWord
declare #newlanguage
if #word <> '' then return #word else
begin
//select new language
case languageNumber of
3 then #newlanguage = 1;
2 then #newlanguage = 3;
1 then #newlanguage = 2;
//search new lenguage
#newWord= SELECT TEXT FROM TRANSLATIONS, PROGDATA
WHERE TRANSLATIONS.TAG="prog" & PROGDATA.ID
AND TRANSLATIONS.LANG=#newlanguage
return #newWord
end;
end;
//FUNCTION CALL
SELECT dbo.checkEmpty(TEXT) FROM TRANSLATIONS, PROGDATA
WHERE TRANSLATIONS.TAG="prog" & PROGDATA.ID
AND TRANSLATIONS.LANG=3
I canabalized the solutions of #fossilcoder and #Smandoli and merged it in one solution:
SELECT
IIf (
NZ(TRANSLATION.Text,"") = "", DEFAULT.TEXT, TRANSLATION.TEXT)
FROM
TRANSLATIONS AS TRANSLATION,
TRANSLATIONS AS DEFAULT,
PROGDATA
WHERE
TRANSLATION.Tag="prog_" & PROGDATA.Id
AND
DEFAULT.Tag="prog" & PROGDATA.Id
AND
TRANSLATION.LanguageId=1
AND
DEFAULT.LanguageId=2
I never thought of referencing a table twice under a different alias

SQLite query for finding file path

I have a column in my SQLite database that contains a file path. Given a portion of the file path, I need to return all the next folders. I would also like to return whether the next portion is the last path in the string (or does not end in a '/'). So if I have the folders:
/my/folder/one
/my/folder/two
/my/folder/path/three
/another/path
/one/two/three
And I have the path:
/my/folder/
The result would return something along the lines of:
+----------+------+
| isLast | item |
+----------+------+
| 1 | one |
| 1 | two |
| 0 | path |
+----------+------+
I've been struggling with this for a while so if anyone can provide any guidance it would be greatly appreciated.
This query:
SELECT item NOT GLOB '*/*' AS isLast,
item
FROM (SELECT substr(MyPath, length('/my/folder/') + 1) AS item
FROM MyTable
WHERE MyPath GLOB '/my/folder/' || '*')
will give you a result like this:
isLast item
------ ----
1 one
1 two
0 path/three
Removing the subpath requires the instr() function, which was only recently introduced in SQLite 3.7.15:
SELECT isLast,
CASE WHEN isLast
THEN item
ELSE substr(item, 1, instr(item, '/') - 1)
END AS item
FROM (SELECT item NOT GLOB '*/*' AS isLast,
item
FROM (SELECT substr(MyPath, length('/my/folder/') + 1) AS item
FROM MyTable
WHERE MyPath GLOB '/my/folder/' || '*'))