Skip to content

Commit

Permalink
Add more input type validation to prevent injection
Browse files Browse the repository at this point in the history
SQL injection was possible on most string types, although it's impact
was limited due to the client connection being `readonly`. The changes
add more robust input validation using a combination of `zod` and type
casts.
  • Loading branch information
0237h committed Nov 3, 2023
1 parent ffbffbe commit b22d8f8
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 13 deletions.
12 changes: 6 additions & 6 deletions src/queries.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DEFAULT_SORT_BY, config } from './config.js';
import { parseBlockId, parseLimit, parseTimestamp } from './utils.js';
import { parseBlockId, parseBlockNumber, parseChain, parseLimit, parseSortBy, parseTimestamp } from './utils.js';
import { chains } from './fetch/openapi.js';

export interface Block {
Expand All @@ -26,16 +26,16 @@ export function getBlock(searchParams: URLSearchParams) {
["less", "<"],
]
for ( const [key, operator] of operators ) {
const block_number = searchParams.get(`${key}_by_block_number`);
const block_number = parseBlockNumber(searchParams.get(`${key}_by_block_number`));
const timestamp = parseTimestamp(searchParams.get(`${key}_by_timestamp`));
if (block_number) where.push(`block_number ${operator} ${block_number}`);
if (timestamp) where.push(`toUnixTimestamp(timestamp) ${operator} ${timestamp}`);
}

// equals
const chain = searchParams.get("chain");
const chain = parseChain(searchParams.get("chain"));
const block_id = parseBlockId(searchParams.get("block_id"));
const block_number = searchParams.get('block_number');
const block_number = parseBlockNumber(searchParams.get('block_number'));
const timestamp = parseTimestamp(searchParams.get('timestamp'));
if (chain) where.push(`chain == '${chain}'`);
if (block_id) where.push(`block_id == '${block_id}'`);
Expand All @@ -47,8 +47,8 @@ export function getBlock(searchParams: URLSearchParams) {

// Sort and Limit
const limit = parseLimit(searchParams.get("limit"));
const sort_by = searchParams.get("sort_by");
query += ` ORDER BY block_number ${sort_by ?? DEFAULT_SORT_BY}`
const sort_by = parseSortBy(searchParams.get("sort_by"));
query += ` ORDER BY block_number ${sort_by}`
query += ` LIMIT ${limit}`

return query;
Expand Down
34 changes: 30 additions & 4 deletions src/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,50 @@
import { expect, test } from "bun:test";
import { parseBlockId, parseLimit, parseTimestamp } from "./utils.js";
import { DEFAULT_MAX_LIMIT } from "./config.js";
import { parseBlockId, parseBlockNumber, parseChain, parseLimit, parseSortBy, parseTimestamp } from "./utils.js";
import { DEFAULT_MAX_LIMIT, DEFAULT_SORT_BY } from "./config.js";

test("parseBlockId", () => {
expect(parseBlockId()).toBeUndefined();
expect(parseBlockId(null)).toBeUndefined();
expect(parseBlockId("invalid")).toBeUndefined();
expect(parseBlockId("00fef8cf2a2c73266f7c0b71fb5762f9a36419e51a7c05b0e82f9e3bacb859bc")).toBe("00fef8cf2a2c73266f7c0b71fb5762f9a36419e51a7c05b0e82f9e3bacb859bc");
expect(parseBlockId("0x00fef8cf2a2c73266f7c0b71fb5762f9a36419e51a7c05b0e82f9e3bacb859bc")).toBe("00fef8cf2a2c73266f7c0b71fb5762f9a36419e51a7c05b0e82f9e3bacb859bc");
});

test("parseBlockNumber", () => {
expect(parseBlockNumber()).toBeUndefined();
expect(parseBlockNumber(null)).toBeUndefined();
expect(parseBlockNumber(-1)).toBeUndefined();
expect(parseLimit("invalid")).toBeNaN();
expect(parseBlockNumber("10")).toBe(10);
expect(parseBlockNumber(10)).toBe(10);
});

test("parseChain", () => {
expect(parseChain()).toBeUndefined();
expect(parseChain(null)).toBeUndefined();
expect(parseChain("' OR 1=1)--")).toBeUndefined();
expect(parseChain("test")).toBe("test");
expect(parseChain("test123")).toBe("test123");
});

test("parseLimit", () => {
expect(parseLimit()).toBe(1);
expect(parseLimit(null)).toBe(1);
expect(parseLimit(-1)).toBe(1);
expect(parseLimit("invalid")).toBeNaN();
expect(parseLimit("10")).toBe(10);
expect(parseLimit(10)).toBe(10);
expect(parseLimit(999999)).toBe(DEFAULT_MAX_LIMIT);
});

test("parseSortBy", () => {
expect(parseSortBy()).toBe(DEFAULT_SORT_BY);
expect(parseSortBy(null)).toBe(DEFAULT_SORT_BY);
expect(parseSortBy("invalid")).toBe(DEFAULT_SORT_BY);
expect(parseSortBy("ASC")).toBe("ASC");
expect(parseSortBy("DESC")).toBe("DESC");
});

test("parseTimestamp", () => {
const seconds = 1672531200;
expect(parseTimestamp()).toBeUndefined();
Expand All @@ -37,5 +65,3 @@ test("parseTimestamp", () => {
expect(() => parseTimestamp(10)).toThrow("Invalid timestamp");
expect(() => parseTimestamp("10")).toThrow("Invalid timestamp");
});


40 changes: 37 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,33 @@
import { config } from "./config.js";
import { z } from 'zod';
import { DEFAULT_SORT_BY, config } from "./config.js";

export function parseBlockId(block_id?: string|null) {
// Match against hexadecimal string (with or without '0x' prefix)
if (!z.string().regex(/^(0x)?[a-fA-F0-9]+$/).safeParse(block_id).success) {
return undefined;
}

return block_id ? block_id.replace("0x", "") : undefined;
}

export function parseBlockNumber(number?: string|null|number) {
let value = undefined;
if (number) {
if (typeof number === "string") value = parseInt(number);
if (typeof number === "number") value = number;
}
// limit must be between 1 and maxLimit
if ( value && value <= 0 ) value = undefined;
return value;
}

export function parseChain(chain?: string|null) {
if (!z.string().regex(/^[a-zA-Z0-9]+$/).safeParse(chain).success) {
return undefined;
}

return chain;
}

export function parseLimit(limit?: string|null|number) {
let value = 1; // default 1
Expand All @@ -7,12 +36,17 @@ export function parseLimit(limit?: string|null|number) {
if (typeof limit === "number") value = limit;
}
// limit must be between 1 and maxLimit
if ( value <= 0 ) value = 1;
if ( value > config.maxLimit ) value = config.maxLimit;
return value;
}

export function parseBlockId(block_id?: string|null) {
return block_id ? block_id.replace("0x", "") : undefined;
export function parseSortBy(sort_by?: string|null) {
if (!z.enum(["ASC", "DESC"]).safeParse(sort_by).success) {
return DEFAULT_SORT_BY;
}

return sort_by;
}

export function parseTimestamp(timestamp?: string|null|number) {
Expand Down

0 comments on commit b22d8f8

Please sign in to comment.