Defeating SQL Injection Attacks in Go: Preventing at scale

Defeating SQL Injection Attacks in Go: Preventing at scale

We all know that the most effective way to prevent SQLi is to use Prepared Statement. But to apply Prepared Statement at scale to all repositories in an organization is not as simple as imagined.

SQL Injection vulnerability

SQL injection (SQLi) is a web security vulnerability that allows an attacker to interfere with the queries that an application makes to its database.

sql.webp

Nguồn: portswigger

Prepared Statement

Sure! Tất cả chúng ta đều biết rằng cách hiệu quả nhất để ngăn chặn SQLi là sử dụng Prepared Statement. Prepared Statement là một tính năng của database cho phép bạn định nghĩa SQL query template với các placeholders cho tham số. SQL query template được biên dịch trước và lưu trữ trên máy chủ cơ sở dữ liệu, sau đó bạn có thể cung cấp các giá trị tham số cụ thể để thực hiện truy vấn nhiều lần mà không cần biên dịch lại.

Quá trình sử dụng Prepared Statement bao gồm 2 bước:

  1. Prepare: Database nhận SQL query template và biên dịch nó vào trong execution plan, tối ưu hóa truy vấn để nâng cao hiệu suất. Các placeholder trong template đại diện các tham số sẽ được cung cấp giá trị sau đó.
  2. Execute: Sau khi Prepared Statement được tạo, bạn có thể cung cấp các giá trị cho tham số để nó thực thi query. Máy chủ database sử dụng precompiled execution plan và chỉ cần thay thế giá trị tham số, làm cho việc thực thi hiệu quả hơn cách thực thi của các SQL query thông thường.

Prepared Statement có thể ngăn SQL injection vì nó tự động xử lý escape và bind param, từ đó coi tham số truyền vào SQL query là data chứ không phải là code có thể thực thi.

Squirrel

Squirrel là một thư viện Golang phổ biến cho việc xử lý SQL query giúp bạn xây dựng SQL query và đơn giản hóa tương tác với database và nâng cao hiệu xuất lập trình. Nhưng điều này không có nghĩa là sử dụng squirrel sẽ mặc định giữ cho ứng dụng của bạn an toàn khỏi SQL injection.

Phạm vi của bài viết này sẽ chỉ tập trung vào việc sử dụng squirrel, với các thư viện khác, bạn vẫn có thể áp dụng tương tự.

Bad practices

func Vul_Func(tableName string, ids []int64, name string) (string, []interface{}, error) {
	idString := make([]string, 0)

	for _, id := range ids {
		idString = append(idString, fmt.Sprintf("%d", id))
	}

	query := squirrel.Select("*").
		From(tableName).
		Where(squirrel.Expr(fmt.Sprintf("id IN (%v)", strings.Join(idString, ",")))).
		Where(squirrel.Expr(fmt.Sprintf("name LIKE '%%%v%%'", name)))

	sqlQuery, args, err := query.ToSql()
	if err != nil {
		log.Fatal(err)
	}
// Print the generated query and arguments
	fmt.Println("query:", sqlQuery)
	fmt.Println("agrs:", args)
}

Đoạn code trên sử dụng hàm fmt.Sptrintf để build query bên trong hàm squirrel.Expr. Việc xây dựng truy vấn như vậy nghĩa là bạn đang nối trực tiếp dữ liệu đầu vào từ user vào truy vấn, đây là cách làm không an toàn và khiến cho đoạn code của bạn tồn tại lỗ hổng. Nếu print query được build theo cách trên ra console thì sẽ trông như sau:

query: SELECT * FROM users WHERE id IN (1,2,3) AND name LIKE '%fuzz OR 1=1%'
args: []

Tham số args là mảng rỗng [] và user input đã được nối trực tiếp vào query.

Good practices

func Safe_Func(tableName string, ids []int64, name string) (string, []interface{}, error) {
	idString := make([]string, 0)

	for _, id := range ids {
		idString = append(idString, fmt.Sprintf("%d", id))
	}

	query := squirrel.Select("*").
		From(tableName).
		Where(squirrel.Expr("id IN ("+squirrel.Placeholders(len(idsArgs))+")", idsArgs...)).
		Where(squirrel.Expr("name LIKE ?", fmt.Sprintf("%%%v%%", name)))

	sqlQuery, args, err := query.ToSql()
	if err != nil {
		log.Fatal(err)
	}
// Print the generated query and arguments
	fmt.Println("query:", sqlQuery)
	fmt.Println("agrs:", args)
}

Để fix lỗi trên, chúng ta cần sử dụng đúng query parameter bằng việc sử dụng placeholder ? cho các tham số. Nếu print query được build theo cách này ra console thì sẽ trông như sau:

query: SELECT * FROM users WHERE id IN (?,?,?) AND name LIKE ?
args: [1 2 3 %fuzz OR 1=1%]

Như bạn có thể thấy, input không an toàn bây giờ đã được truyền vào arg thay vì nối trực tiếp vào câu truy vấn.

Detect using string builder at scale

Nếu bạn đang đảm nhận task phát hiện và fix tất cả các vị trí sử dụng string builder như ví dụ ở phần bad practices, bạn sẽ làm gì? Bạn có thể đi hỏi tất cả các Lập trình viên trong công ty để nhớ lại tất cả các vị trí mình đã sử dụng string builder để xử lý.

Nhìn chung, có nhiều cách để chúng ta làm xong task nhưng thời gian của chúng ta không nhiều. Nếu bạn tinh ý, bạn có thể thấy giải pháp cho task này thông qua 2 ví dụ mà tôi show ở trên. Giải pháp là scan tất cả query được xử lý bởi driver và nếu query thỏa mãn 2 tiêu chí sau, nó là query tồn tại lỗ hổng SQL injection:

  1. query sử dụng mệnh đề where
  2. Không chứa bất kỳ arguments, giá trị args là một mảng rỗng như trong ví dụ Bad practices

prevent_sqli.webp

Logic giả mã cho phương pháp này được trình bày như sau:

sqlQuery, args, err := query.ToSql()
lenArgs = len(args)
if strings.Contains(query, "where") && lenArgs == 0 {
   // return Alert location is using string builder to build query.
}

Cảm ơn đã bạn đã đọc!