Fix sql injection on Rails model scope - sql

There is a model scope I have been trying to refactor, Brakeman is complaining about it so I thought it was a good idea to fix it since we were scanned by bots who are looking for our site vulnerabilities.
scope :cash_deal_aggregated, -> (filter = '') {
select("deals.*")
.from([Arel.sql(
"(SELECT DISTINCT ON (COALESCE(cash_deal_details.cash_deal_id, 0.1*deals.id)) deals.*
FROM deals
INNER JOIN portfolios ON portfolios.id = deals.portfolio_id
LEFT JOIN cash_deal_details ON deals.cash_deal_detail_id = cash_deal_details.id
#{filter}) deals"
)]
)
}
The scope above is used like this:
filter = "WHERE portfolios.client_id = #{client_id}"
deal_records = deal_records = Deal.cash_deal_aggregated(filter)
And it is also used like this:
deal_records = Deal.cash_deal_aggregated
Initially I tried to fix it by adding the filter directly in the query but then got multiple errors.
Appreciate your suggestions for this refactor.

Wrap ActiveRecord's connection.quote(), wrap client_id in this method, e.g., in your case, try this
"WHERE portfolios.client_id = #{connection.quote(client_id)}"
I also got these errors from brakeman earlier and this resolved it.

This is the refactor. Credits to Rajdeep Singh
scope :cash_deal_aggregated, -> (client_id = nil) {
filter = "WHERE portfolios.client_id = #{connection.quote(client_id)}" if client_id
select("deals.*")
.from([Arel.sql(
"(SELECT DISTINCT ON (COALESCE(cash_deal_details.cash_deal_id, 0.1*deals.id)) deals.*
FROM deals
INNER JOIN portfolios ON portfolios.id = deals.portfolio_id
LEFT JOIN cash_deal_details ON deals.cash_deal_detail_id = cash_deal_details.id
#{filter}) deals"
)]
)
}

Related

How to use joins with `where` condition

I want to get users with their assets, which have type 'Google'.
I tried
User.joins(:assets).where(assets: { assetable_type: 'Google' })
or
User.joins(:assets).where("assets.assetable_type = 'Google'")
But that scopes work identically and return nothing.
SQL, which scopes generate:
SELECT "users".* FROM "users" INNER JOIN "assets" ON "assets"."assetable_id" = "users"."id" AND "assets"."assetable_type" = 'User' WHERE (assets.assetable_type = 'Google')
It doesn't seem to look right
What am I doing wrong?
You didn't gave any details about your models and associations
So, I can give you a temporary solution to use raw sql
sql = "SELECT users.* FROM users INNER JOIN assets ON assets.assetable_id = users.id WHERE assets.assetable_type = 'Google'"
result = ActiveRecord::Base.connection.execute(sql).to_a
Note: If you are using relation as per rails standards it should be
sql = "SELECT users.* FROM users INNER JOIN assets ON assets.assetable_id = users.id WHERE assets.assetable_type = 'Google'"
result = ActiveRecord::Base.connection.execute(sql).to_a
But I think you are using another name i.e., assetable_id instead of user_id.

How to perform joins in NHibernate with paging

I'm performing some maintenance on some code that uses NHibernate as a person who knows almost nothing about NHibernate...
I have the following query
var query = string.Format(#"select s.Id, s.Iccid, c.Name as Carrier, aa.StartDate as AssignmentDate, cust.Name as AssignedCustomerName
from assetassignment aa
left join SIM s on aa.AssetId = s.Id
left join Carrier c on s.CarrierId = c.Id
left join customer cust on aa.CustomerId = cust.Id
where aa.enddate is null
and aa.CustomerId in ({0})
and s.dateremoved is null",
string.Join(",",idsToInclude));
if (!string.IsNullOrWhiteSpace(carrier))
{
query += " and c.Name = '" + carrier + "'";
}
var results = _session.CreateSQLQuery(query)
.SetResultTransformer(new AliasToBeanResultTransformer(typeof(HomepageSIMTableRow)))
.List<HomepageSIMTableRow>();
return results;
This works fine for me (and means I didn't have to grok NHibernate to get something running I could work against but now I need to add paging and it is just feeling smelly.
Any guidance on how to move this into NHibernate land and add paging would be awesome!
I'm not sure if this works with regular SQL, but usually with NHibernate you add a
var results = _session.CreateSQLQuery(query)
.SetFirstResult(0)
.SetSetMaxResults(30)
.SetResultTransformer(new AliasToBeanResultTransformer(typeof(HomepageSIMTableRow)))
.List<HomepageSIMTableRow>();
This works for regular Criterias and HQL queries.
You can read this as a reference: How can you do paging with NHibernate?
The reason this feels "smelly" is because you're writing SQL and passing it straight to the ORM.
NH offers a whole mechanism for paging at an entity level. I have found this to get a little tricky when you're eagerly loading other entities though.
My suggestion would be to either:
Write the pagination SQL yourself, this is probably lower risk as it will involve less changes
Convert the whole query to use NH ICriterion query or a HQL statement.
Unfortunately it's hard to suggest which one without knowing the risk/situation.

Creating a Rails 3 scope that joins to a subquery

First off, I'm a Ruby/Rails newbie, so I apologize if this question is basic.
I've got a DB that (among other things) looks like this:
organizations { id, name, current_survey_id }
surveys { id, organization_id }
responses { id, survey_id, question_response_integer }
I'm trying to create a scope method that adds the average of the current survey answers to a passed-in Organization relation. In other words, the scope that's getting passed into the method would generate SQL that looks like more-or-less like this:
select * from organizations
And I'd like the scope, after it gets processed by my lambda, to generate SQL that looks like this:
select o.id, o.name, cs.average_responses
from organizations o join
(select r.id, avg(r.question_response_integer) as average_responses
from responses r
group by r.id) cs on cs.id = o.current_survey_id
The best I've got is something like this:
current_survey_average: lambda do |scope, sort_direction|
average_answers = Responses.
select("survey_id, avg(question_response_integer) as average_responses").
group("survey_id")
scope.joins(average_answers).order("average_responses #{sort_direction}")
end
That's mostly just a stab in the dark - among other things, it doesn't specify how the scope could be expected to join to average_answers - but I haven't been able to find any documentation about how to do that sort of join, and I'm running out of things to try.
Any suggestions?
EDIT: Thanks to Sean Hill for the answer. Just to have it on record, here's the code I ended up going with:
current_survey_average: lambda do |scope, sort_direction|
scope_table = scope.arel.froms.first.name
query = <<-QUERY
inner join (
select r.survey_id, avg(r.question_response_integer) as average_responses
from responses r
group by r.survey_id
) cs
on cs.survey_id = #{scope_table}.current_survey_id
QUERY
scope.
joins(query).
order("cs.average_responses #{sort_direction}")
end
That said, I can see the benefit of putting the averaged_answers scope directly onto the Responses class - so I may end up doing that.
I have not been able to test this, but I think the following would work, either as-is or with a little tweaking.
class Response < ActiveRecord::Base
scope :averaged, -> { select('r.id, avg(r.question_response_integer) as average_responses').group('r.id') }
scope :current_survey_average, ->(incoming_scope, sort_direction) do
scope_table = incoming_scope.arel.froms.first.name
query = <<-QUERY
INNER JOIN ( #{Arel.sql(averaged.to_sql)} ) cs
ON cs.id = #{scope_table}.current_survey_id
QUERY
incoming_scope.joins(query).order("average_responses #{sort_direction}")
end
end
So what I've done here is that I have split out the inner query into another scope called averaged. Since you do not know which table the incoming scope in current_survey_average is coming from, I got the scope table name via scope.arel.froms.first.name. Then I created a query string that uses the averaged scope and joined it using the scope_table variable. The rest is pretty self-explanatory.
If you do know that the incoming scope will always be from the organizations table, then you don't need the extra scope_table variable. You can just hardcode it into the join query string.
I would make one suggestion. If you do not have control over sort_direction, then I would not directly input that into the order string.

using a placeholder with joins

I'm attempting to avoid any SQL injection vulnerabilities by substituting with my params on a join.
Category.joins("LEFT OUTER JOIN incomes ON incomes.category_id = categories.id AND incomes.dept_id = ?", params[:Dept])
This attempts to execute the query with a question mark in it, instead of substituting it for the param. What is the proper way to do this?
EDIT:
Query needs to return this:
SELECT categories.*
FROM "categories"
LEFT OUTER JOIN incomes
ON incomes.category_id = categories.id AND incomes.dept_id = 86
not
SELECT categories.*
FROM "categories"
LEFT OUTER JOIN incomes
ON incomes.category_id = categories.id
WHERE incomes.dept_id = 86
Very different results!
One option is to use the sanitize_sql_array method. It is, however, a protected method so on your Category model you could do:
class Category < ActiveRecord::Base
def self.income_for_dept(dept)
Category.joins(sanitize_sql_array(["LEFT OUTER JOIN incomes ON incomes.category_id = categories.id AND incomes.dept_id = ?", dept]))
end
end
Then you would call it like:
Category.income_for_dept(params[:Dept])
Ruby provides some other methods, if need be, to get at that method without making a class method in Category.
Try
Category.joins(:incomes).where(:incomes => { :dept_id => params[:Dept] })
And check out the Rails documentation for joining tables.

Specifying left join conditions using rails include syntax

Is there a more railsy way to do this query in rails 3?
scope :unblocked_on_invite, joins(
"LEFT JOIN blockers
ON blockers.member_id = members.id
AND blockers.type = 'InviteBlocker'").where("blockers.id IS NULL")
If you use :include it will perform an automatic INNER JOIN. As far as LEFT JOIN goes you are doing exactly what you should be doing. The only way I can see to make this more railsy is to write it like this:
scope :unblocked_on_invite, joins(
"LEFT JOIN blockers
ON blockers.member_id = members.id
AND blockers.type = 'InviteBlocker'").where(:blockers => nil)