API Levels in DragonRuby Game Toolkit
DragonRuby Game Toolkit (DRGTK) is a 2D game engine built with mRuby, SDL, and LLVM. It’s meant to be tiny, fast, and allow you to turn out games quickly using Ruby.
Unfortunately, since the documentation is focused on making games quickly, I sometimes get lost when trying to figure out how to do things that should be simple. DragonRuby seems to have borrowed Perl’s “There’s more than one way to do it” philosophy because for anything you want to do with the API there are several ways to do it.
The documentation and examples tend to focus on the simplest forms (which is fine) but then require digging and experimentation to figure out the rest.
To help explain/document the different API options, this post will go through different methods of rendering images (well, rectangles mostly).
API Levels
Level 0 - Getting Started
The main object one interacts with in DragonRuby is canonically called args
(always accessible with $gtk.args
… because there’s more than one way!)
To output things, like shapes, sprites, or sounds, you can use the “shovel” operator <<
on args.outputs
- like args.outputs.sprites
or args.outputs.sounds
.
For these “basic” objects, you’ll need to shovel things in on every “tick” of the game engine.
This is a complete DragonRuby example to output a rectangle:
def tick args
args.outputs.solids << [100, 200, 300, 400]
end
So far, so good.
(You can assume the rest of the examples below are inside a tick
method if it’s not explicitly defined.)
Level 1 - Arrays
The documentation usually starts off by passing things to args.outputs
as arrays - essentially positional arguments.
For example:
args.outputs.solids << [100, 200, 300, 400]
What does that do? I’m not quite sure!
Okay - it shows a black rectangle on the screen. Not that exciting, but useful enough for our examples.
The problem with using arrays though is remembering which index in the array is which attribute. On top of that, it’s not even recommended to pass in arrays because they are slow (for some reason).
Level 2 - Hashes
What is better than arrays? Hashes! (Hash tables/associative arrays for anyone not familiar with Ruby.)
args.outputs.solids << {
x: 100,
y: 200,
w: 300, # width
h: 400 # height
}
Okay, that’s way easier to understand!
And there are more options, too:
args.outputs.solids << {
x: 100,
y: 200,
w: 300,
h: 400,
r: 255, # red
g: 200, # green
b: 255, # blue
a: 100, # alpha
blendmode_enum: 0 # blend mode
}
Level 3 - Primitives
But there is yet another way… instead of using args.outputs.solids
, args.outputs.labels
, args.outputs.sprites
, etc., we can output a hash to args.outputs.primitives
but mark it as the right primitive “type”:
args.outputs.primitives << {
x: 100,
y: 200,
w: 300,
h: 400,
r: 255, # red
g: 200, # green
b: 255, # blue
a: 100, # alpha
blendmode_enum: 0 # blend mode
}.solid!
Weird, but okay. Why might one want to do this? See the “Layers” section down below!
Level 4 - Classes
Finally, probably the most natural for a Rubyist: just use a class!
To do this, you must define all the methods expected for the type of primitive, plus define a method called primitive_marker
that returns the type of primitive.
class ACoolSolid
attr_reader :x, :y, :w, :h, :r, :g, :b, :a, :blendmode_enum
def initialize x, y, w, h
@x = x
@y = y
@w = w
@h = h
end
def primitive_marker
:solid
end
end
def tick args
args.outputs.primitives << ACoolSolid.new(100, 200, 300, 400)
end
Instead of defining a bunch of methods with attr_reader
, you can use attr_sprite
instead which is a DragonRuby shortcut method to do the same thing.
Layers
DragonRuby renders outputs in this order (from back to front):
- Solids
- Sprites
- Primitives
- Labels
- Lines
- Borders
For each “layer” the objects are rendered in FIFO order - the first things in the queue are rendered first.
But wait… one of these things is not like the others. Doesn’t primitives
just hold things like solids, sprites, labels…?
Yes!
But using primitives
enables better control over render order.
For example, what if we want to render a rectangle on top of a sprite? With the fixed rendering order above, it’s impossible! But by using args.outputs.primitives
we can do it:
def tick args
a_solid = {
x: 100,
y: 200,
w: 300,
h: 400
}.solid!
a_sprite = {
x: 100,
y: 200,
w: 500,
h: 500,
path: 'metadata/icon.png'
}.sprite!
args.outputs.primitives << a_sprite << a_solid
end
And here’s the proof:
Every Tick?
args.outputs.sprites
, etc. get cleared after each call to tick
. So every tick we have to recreate all the objects and pass them in to args.outputs
. Seems wasteful, right? Yes, it is!
It’s somewhat odd that most DragonRuby examples show creating arrays or hashes for primitives each tick. It made me think somehow the rendering process was destructive - were the things added into args.outputs
destroyed or modified in some way?
Turns out, no. It is fine to create e.g. a sprite representation once and render the same object each time.
Here we’ll use a global for demonstration purposes:
class ACoolSolid
attr_reader :x, :y, :w, :h, :r, :g, :b, :a, :blendmode_enum
def initialize x, y, w, h
@x = x
@y = y
@w = w
@h = h
end
def primitive_marker
:solid
end
end
$a_solid = ACoolSolid.new(10, 20, 30, 40)
def tick args
args.outputs.primitives << $a_solid
end
But Wait…
We are still shoveling an object into args.outputs.primitives
each time. Surely that is unnecessary?
Correct! There are static versions for each args.outputs
(e.g. args.outputs.static_solids
) that do not get cleared every tick.
Naturally, this is more efficient than creating objects and updating the outputs 60 times per second.
We’ll explore these options in a future post, but be aware they are available!
Fixing Just One False Positive in Brakeman
A while ago, I came across a Brakeman false positive that I wanted to fix.
For just one false positive, it became a bit of an epic journey.
The code looked something like this:
class Task < ApplicationRecord
enum status: {
pending: 0,
success: 1,
failed: 3,
}
NOT_FAILURES = ['pending', 'success'].freeze
end
class TaskRunner
def get_failures
start_time = Date.beginning_of_quarter
end_time = Date.end_of_quarter
no_failure_enums = Task.statuses.values_at(*Task::NOT_FAILURES)
query = <<~QUERY
SELECT COUNT(*)
FROM `tasks`
WHERE `tasks`.`status` NOT IN (#{no_failure_enums.join(',')})
AND `tasks`.`time_end` BETWEEN #{start_time} AND #{end_time}
QUERY
# Line below triggers a SQL injection warning
Task.connection.select_all(query)
end
end
(You can imagine in reality the query is a bit more complicated and justifies writing it this way.)
This code results in an SQL injection warning from Brakeman:
Confidence: High
Category: SQL Injection
Check: SQL
Message: Possible SQL injection
Code: Task.connection.select_all("SELECT COUNT(*)\nFROM `filings`\nWHERE `filings`.`status` NOT IN (#{Task.statuses.values_at(*["pending", "success"]).join(",")})\nAND `filings`.`time_end` BETWEEN #{Date.beginning_of_quarter} AND #{Date.end_of_quarter}\n")
File: app/models/task.rb
Line: 25
This is a little hard to read, so let’s take a look at a better formatted version of the code Brakeman is complaining about:
Task.connection.select_all(
"SELECT COUNT(*) \
FROM `filings` \
WHERE `filings`.`status` NOT IN (#{Task.statuses.values_at(*["pending", "success"]).join(",")}) \
AND `filings`.`time_end` BETWEEN #{Date.beginning_of_quarter} \
AND #{Date.end_of_quarter}"
)
Brakeman is warning about this SQL query because it is using string interpolation to unsafely add in values to the query. If an attacker could control those values, they could modify the SQL run by the database.
In particular, it’s warning about
Task.statuses.values_at(*["pending", "success"]).join(",")
(the value in no_failure_enums.join(',')
).
However, in this case, we know that Task.statuses
is actually a constant - it’s defined using enum
in the Task
class. This code is just grabbing the integer values for the given enums and joining them back into a comma-separated string.
So how do we get Brakeman to understand that this value is actually safe?
Splatted Arrays
Let’s dive in!
The call we care about:
Task.statuses.values_at(*["pending", "success"]).join(",")
First up is *["pending", "success"]
. This code converts an array of strings to individual method arguments (i.e., values_at("pending", "success")
.
This is pretty easy to handle. In the case where a splatted array is the only argument to a method, just use the elements of the array as the argument list. (Check out the pull request here)
This gets us to:
Task.statuses.values_at("pending", "success").join(",")
Better!
Hash Values
In this case, values_at
is Hash#values_at
- it returns an array of values from the hash table for the given keys.
This is also not too difficult to implement. I went ahead and covered Hash#values
at the same time. (Check out the pull request here)
Brakeman will now do something like this:
h = { a: 1, b: 2, c: 3 }
h.values_at(:a, :c) #=> [1, 3]
Great! Back to our code sample, how does it look now?
Task.statuses.values_at("pending", "success").join(",")
Oh… it looks exactly the same because Brakeman has no idea what Task.statuses
is.
Okay, no problem. We just need to implement support for ActiveRecord’s enum
.
Detour!
Here I took a little detour. Task.statuses
is a method that returns a hash value. Instead of just implementing that, I thought this would be a good time to support methods with single, simple return values.
For example:
class Dog
def self.sound
'bark'
end
end
If Brakeman could know that Dog.sound
returns 'bark'
, I could implement enums as method definitions that return simple arrays or hashes. (More on this later!)
To implement this functionality, I rewrote how Brakeman tracks methods (as real objects) and updated some method lookup code.
The details aren’t particularly interesting, but the code changes are here.
Back to Enums
Calling enum
essentially defines a bunch of methods.
For example this code:
class Task < ApplicationRecord
enum status: {
pending: 0,
success: 1,
failed: 3,
}
end
Will define methods like
Task.statuses # the one we care about!
Task.status
Task#status
Task#pending?
Task#success?
# ..etc.
You can pass in an explicit hash mapping keys to values or just an array of keys and Rails will do the mapping.
To implement this in Brakeman, we simulate the creation of the status
and statuses
methods:
class Task < ApplicationRecord
def self.statuses
{
pending: 0,
success: 1,
failed: 3,
}
end
end
Then rely on the previous changes for Task.statuses
now returning a hash.
Another Detour
You might have noticed that the enum definition uses status
but we need statuses
.
This required a tiny tweak to Brakeman’s extremely over-simplified pluralize
.
Back to the False Positive
Where are we now?
With enum
support and proper pluralization, this code:
Task.statuses.values_at(*["pending", "success"]).join(",")
Gets reduced like this:
{
pending: 0,
success: 1,
failed: 3,
}.values_at(*["pending", "success"]).join(",")
to
{
pending: 0,
success: 1,
failed: 3,
}.values_at("pending", "success").join(",")
to
[:BRAKEMAN_SAFE_LITERAL, :BRAKEMAN_SAFE_LITERAL].join(",")
to
"BRAKEMAN_SAFE_LITERAL,BRAKEMAN_SAFE_LITERAL"
Well… it’s not perfect. Note that the array values are strings, but our enum uses symbol keys. But Brakeman knows the enum is all literal values which are safe, so we end up with this.
The query now looks like:
Task.connection.select_all(
"SELECT COUNT(*) \
FROM `filings` \
WHERE `filings`.`status` NOT IN (#{"BRAKEMAN_SAFE_LITERAL,BRAKEMAN_SAFE_LITERAL"}) \
AND `filings`.`time_end` BETWEEN #{Date.beginning_of_quarter} \
AND #{Date.end_of_quarter}"
)
Again, not perfect but at least Brakeman isn’t going to warn about interpolating a string literal into the query.
Not Done Yet
Ah, but wait. The warning is not gone!
Confidence: Medium
Category: SQL Injection
Check: SQL
Message: Possible SQL injection
Code: Task.connection.select_all("SELECT COUNT(*)\nFROM `filings`\nWHERE `filings`.`status` NOT IN (#{"BRAKEMAN_SAFE_LITERAL,BRAKEMAN_SAFE_LITERAL"})\nAND `filings`.`time_end` BETWEEN #{Date.beginning_of_quarter} AND #{Date.end_of_quarter}\n")
File: app/models/task.rb
Line: 25
What’s wrong now?
Brakeman doesn’t know what Date.beginning_of_quarter
or Date.end_of_quarter
are, so it generates a lower confidence warning about it. For SQL injection, Brakeman is pretty paranoid about any string interpolation, even if it’s not sure the values are “dangerous”.
But anything coming from Date
is likely to be safe, so now Brakeman ignores Date
calls in SQL.
Whew. Done?
Yep - now that code will no longer warn.
Except… in the months it took me to address this false positive, the code has changed in such a way that Brakeman now has a false negative problem (should be warning about some things, but isn’t). But that’s a different problem for a different day.
At least that one false positive is fixed!
Rails 6.1 SQL Injection Updates
Since early 2013, I have been maintaining rails-sqli.org, a collection of Rails ActiveRecord methods that can be vulnerable to SQL injection.
Rails 6 has been out since December 2019, but sadly the site has been missing information about changes and new methods in Rails 6.
As that deficiency has recently been rectified, let’s walk through what has changed since Rails 5!
delete_all
, destroy_all
In earlier versions of Rails, delete_all
and destroy_all
could be passed a string of raw SQL.
In Rails 6, these two methods no longer accept any arguments.
Instead, you can use…
delete_by
, destroy_by
New in Rails 6, delete_by
and destroy_by
accept the same type of arguments as where
: a Hash, an Array, or a raw SQL String.
This means they are vulnerable to the same kind of SQL injection.
For example:
params[:id] = "1) OR 1=1--"
User.delete_by("id = #{params[:id]}")
Resulting query that deletes all users:
DELETE FROM "users" WHERE (id = 1) OR 1=1--)
order
, reorder
Prior to Rails 6, it was possible to pass arbitrary SQL to the order
and reorder
methods.
Since Rails did not offer an easy way of setting sort direction, this kind of code was common:
User.order("name #{params[:direction]}")
In Rails 6.0, injection attempts would raise a deprecation warning:
DEPRECATION WARNING: Dangerous query method (method whose arguments are used as raw SQL) called with non-attribute argument(s): "--". Non-attribute arguments will be disallowed in Rails 6.1. This method should not be called with user-provided values, such as request parameters or model attributes. Known-safe values can be passed by wrapping them in Arel.sql().
Starting with Rails 6.1, some logic to check the arguments to order
. If the arguments do not appear to be column names or sort order, they will be rejected:
> User.order("name ARGLBARGHL")
Traceback (most recent call last):
1: from (irb):12
ActiveRecord::UnknownAttributeReference (Query method called with non-attribute argument(s): "name ARGLBARGHL")
It is still possible to inject additional columns to extract some information from the table, such as number of columns or names of the columns:
params[:direction] = ", 8"
User.order("name #{params[:direction]}")
Resulting exception:
ActiveRecord::StatementInvalid (SQLite3::SQLException: 2nd ORDER BY term out of range - should be between 1 and 7)
pluck
pluck
pulls out specified columns from a query, instead of loading whole records.
In previous versions of Rails, pluck
(somewhat surprisingly!) accepted arbitrary SQL strings if they were passed in as an array.
Like order
/reorder
, Rails 6.0 started warning about this:
> User.pluck(["1"])
DEPRECATION WARNING: Dangerous query method (method whose arguments are used as raw SQL) called with non-attribute argument(s): ["1"]. Non-attribute arguments will be disallowed in Rails 6.1. This method should not be called with user-provided values, such as request parameters or model attributes. Known-safe values can be passed by wrapping them in Arel.sql().
In Rails 6.1, pluck
now only accepts attribute names!
reselect
Rails 6 introduced reselect
, which allows one to completely replace the SELECT
clause of a query. Like select
, it accepts any SQL string. Since SELECT
is at the very beginning of the SQL query, it makes it a great target for SQL injection.
params[:column] = "* FROM orders -- "
User.select(:name).reselect(params[:column])
Note this selects all columns from a different table:
SELECT * FROM orders -- FROM "users"
rewhere
rewhere
is analogous to reselect
but it replaces the WHERE
clause.
Like where
, it is very easy to open up rewhere
to SQL injection.
params[:age] = "1=1) OR 1=1--"
User.where(name: "Bob").rewhere("age > #{params[:age]}")
Resulting query:
SELECT "users".* FROM "users" WHERE "users"."name" = ? AND (age > 1=1) OR 1=1--)
Wrapping Up
Any other new methods that allow SQL injection? Let me know!
Want to find out more?
- rails-sqli.org for the complete list.
- Brakeman to help find vulnerable queries in your code.
- OWASP SQL Injection Cheat Sheet to learn more about preventing SQL injection.