使用 Golang 防止 SQL 注入
我们将涵盖哪些内容?
SQL注入是攻击系统最常用的技术之一,我们可以利用它在易受攻击的端点上执行恶意SQL,从而操纵数据库。虽然已经有很多方法可以缓解这种攻击,但如果开发人员不注意,仍然可能留下漏洞。
大多数 ORM 已经能够抑制这种类型的攻击,但在 Go 语言中,不使用 ORM 的情况非常普遍,如果处理不当,这种漏洞发生的几率会更大。
SQL注入的工作原理
SQL 注入通常发生在提供了允许传递参数的过滤器,但这些参数没有被正确处理的情况下,例如:
GET http://localhost:8080/users?id=1 HTTP/1.1
此端点通过 ID 搜索用户,但如果处理不当,可能会执行 SQL 注入攻击。
GET http://localhost:8080/users?id='1'OR'1'='1' HTTP/1.1
'1'OR'1'='1'添加忽略任何条件并返回 true 布尔值的SQL 语句'1'='1,这样查询总是返回数据库中的所有用户,这种漏洞很严重,我们甚至可以删除整个数据库。
创造种子
为了便于测试,我们将创建一个种子数据(在表中填充记录),为了方便测试,我们将在数据库seed.go文件夹中创建一个文件:
func SeedUsers() error {
// drop table users
_, err := DBConnection.Exec(`DROP TABLE IF EXISTS users`)
if err != nil {
log.Fatal(err)
}
// create table users
createTableQuery := `
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(256) NOT NULL,
email VARCHAR(256) NOT NULL UNIQUE,
password VARCHAR(256) NOT NULL
)
`
_, err = DBConnection.Exec(createTableQuery)
if err != nil {
log.Fatal(err)
}
log.Println("Tabela de usuários criada com sucesso.")
insertUserQuery := `
INSERT INTO users (name, email, password) VALUES
('John Doe', 'john.doe@example.com', 123456),
('Bob', 'bob@example.com', 123456),
('Charlie', 'charlie@example.com', 123456),
('Slash', 'slash@example.com', 098765),
('Gilmour', 'gilmour@example.com', 1255657),
('Steve Vai', 'steve_vai@example.com', 1255657)
`
_, err = DBConnection.Exec(insertUserQuery)
if err != nil {
log.Fatal(err)
}
log.Println("Usuários inseridos com sucesso.")
return nil
}
SeedUsers如果表已存在,则将其删除;如果存在,则重新创建该表;最后将用户添加到表中。如有需要,您可以添加更多用户。
项目结构
为了说明这一点,让我们创建一些具有此漏洞的端点并修复这些漏洞,但首先让我们构建一下项目结构,我不会深入讲解结构,我会把仓库链接放在这里。
这将是我们项目的结构,我们将使用 PostgreSQL 作为数据库,使用go chi创建我们的端点,使用go dot env导入我们的环境变量。
我们分别main.go启动服务器、连接银行和我们的终端节点:
func main() {
err := database.NewDBConnection()
if err != nil {
panic(err)
}
service.SeedUsers("test")
r := chi.NewRouter()
r.Get("/users", handler.GetUsersInjectHandler)
r.Get("/users/correct", handler.GetUsersCorrectHandler)
r.Delete("/users", handler.DeleteUserInjectHandler)
r.Delete("/users/correct", handler.DeleteUserCorrectHandler)
server := &http.Server{
Addr: ":8080",
Handler: r,
}
server.ListenAndServe()
}
connection.go我们将建立与 PostgreSQL 的连接,并使该连接在我们的应用程序中全局可用:
var DBConnection *sql.DB
func NewDBConnection() error {
err := godotenv.Load(".env")
if err != nil {
return errors.New("error loading .env file")
}
databaseURL := os.Getenv("DATABASE_URL")
db, err := sql.Open("postgres", databaseURL)
if err != nil {
return err
}
DBConnection = db
return nil
}
user_handler.go他将负责处理我们的请求并联系user_service.go服务部门。
创建端点
让我们创建带有漏洞的端点和一个没有漏洞的端点。
搜索用户
让我们创建一个按 id 搜索用户的端点,我们称之为GetUsersInjectHandler,这个端点将存在我们的漏洞。
user_handler.go:
func GetUsersInjectHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "id not provided", http.StatusBadRequest)
return
}
users, err := service.GetUserInject(id)
if err != nil {
fmt.Println(err)
http.Error(w, "Error when searching for users", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(users); err != nil {
http.Error(w, "Error encoding users to JSON", http.StatusInternalServerError)
return
}
}
user_service.go:
func GetUserInject(id string) ([]User, error) {
query := fmt.Sprintf("SELECT id, name, email FROM users WHERE id = %s", id)
rows, err := database.DBConnection.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var users []User
for rows.Next() {
var user User
if err := rows.Scan(&user.ID, &user.Name, &user.Email); err != nil {
return nil, err
}
users = append(users, user)
}
return users, nil
}
在执行查询的服务中,我们直接传递 id "SELECT id, name, email FROM users WHERE id = %s", id,这里存在一个漏洞,即我们直接将 id 传递给查询,而没有先对其进行处理。
让我们使用 vscode HTTP Client扩展发出一个请求:
GET http://localhost:8080/users?id=1 HTTP/1.1
content-type: application/json
我们收到了预期的回报:
[
{
"id": 1,
"name": "John Doe",
"email": "john.doe@example.com"
}
]
现在,让我们注入 SQL 语句:
GET http://localhost:8080/users?id='1'OR'1'='1' HTTP/1.1
content-type: application/json
已接收所有用户:
[
{
"id": 1,
"name": "John Doe",
"email": "john.doe@example.com"
},
{
"id": 2,
"name": "Bob",
"email": "bob@example.com"
},
{
"id": 3,
"name": "Charlie",
"email": "charlie@example.com"
},
{
"id": 4,
"name": "Slash",
"email": "slash@example.com"
},
{
"id": 5,
"name": "Gilmour",
"email": "gilmour@example.com"
},
{
"id": 6,
"name": "Steve Vai",
"email": "steve_vai@example.com"
}
]
由于注入的 SQL 语句'1'='1'始终是 NULL true,因此查询始终会返回所有记录。
删除用户
上述 SQL 语句存在严重缺陷,但如果同样的漏洞存在于删除记录的查询中,情况可能会更糟。我们来看一个例子:
user_handler.go:
func DeleteUserInjectHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "id not provided", http.StatusBadRequest)
return
}
err := service.DeleteUserInject(id)
if err != nil {
fmt.Println(err)
http.Error(w, "Error when searching for users", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode("Usuário deletado com sucesso"); err != nil {
http.Error(w, "Error encoding users to JSON", http.StatusInternalServerError)
return
}
}
user_service.go:
func DeleteUserInject(id string) error {
query := fmt.Sprintf("DELETE FROM users WHERE id = %s", id)
_, err := database.DBConnection.Exec(query)
if err != nil {
return err
}
return nil
}
我们使用相同的逻辑按 ID 删除用户:
DELETE http://localhost:8080/users?id=1 HTTP/1.1
content-type: application/json
调用此端点后,用户id = 1将被删除,现在让我们注入 SQL 语句:
DELETE http://localhost:8080/users?id='1'OR'1'='1' HTTP/1.1
content-type: application/json
当使用注入的 SQL 调用端点时,请注意用户表中的所有记录都被删除了,你能想象这会对生产数据库造成多大的损害吗?
这可以在任何查询中实现,我们可以设置一个端点,通过 ID 更新用户的密码,然后利用 SQL 注入更新所有用户的密码,可能性是无穷无尽的!
修复漏洞
已经有很多方法可以缓解这种攻击,例如,我们可以在处理程序中处理它,并检查值中是否包含 SQL id,但最有效和正确的方法是使用数据库驱动程序的现有资源,在这种情况下,该软件包"database/sql"已经具有此功能,只需进行少量修改即可避免 SQL 注入攻击。
搜索用户
让我们也遵循同样的逻辑:
user_handler.go
func GetUsersCorrectHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "id not provided", http.StatusBadRequest)
return
}
users, err := service.GetUserCorrect(id)
if err != nil {
fmt.Println(err)
http.Error(w, "Error when searching for users", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(users); err != nil {
http.Error(w, "Error encoding users to JSON", http.StatusInternalServerError)
return
}
}
user_service.go:
func GetUserCorrect(id string) ([]User, error) {
query := "SELECT id, name, email FROM users WHERE id = $1"
rows, err := database.DBConnection.Query(query, id)
if err != nil {
return nil, err
}
defer rows.Close()
var users []User
for rows.Next() {
var user User
if err := rows.Scan(&user.ID, &user.Name, &user.Email); err != nil {
return nil, err
}
users = append(users, user)
}
return users, nil
}
然而,现在我们不id直接传递参数,而是传递一个标记,该标记$1表明该位置将有一个参数,然后将其传递给查询。Query(query, id)这样,驱动程序就已经执行了我们所谓的“预处理语句”或“清理”、“水合”(您可以随意称呼它),这种方法有助于将查询逻辑与数据输入分离,从而提高数据库操作的安全性和完整性。
让我们调用端点并尝试注入 SQL 语句:
GET http://localhost:8080/users/correct?id='1'OR'1'='1' HTTP/1.1
content-type: application/json
我们收到错误提示并避免了 SQL 注入!
HTTP/1.1 500 Internal Server Error
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Wed, 27 Dec 2023 15:53:27 GMT
Content-Length: 28
Connection: close
Error when searching for users
搜索正确:
GET http://localhost:8080/users/correct?id=2 HTTP/1.1
content-type: application/json
现在我们能够正确接收用户数据了。
[
{
"id": 2,
"name": "Bob",
"email": "bob@example.com"
}
]
删除用户
让我们使用与搜索用户时相同的解决方案。
user_handler.go
func DeleteUserCorrectHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
http.Error(w, "meyhod not allowed", http.StatusMethodNotAllowed)
return
}
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "id not provided", http.StatusBadRequest)
return
}
err := service.DeleteUserCorrect(id)
if err != nil {
fmt.Println(err)
http.Error(w, "Error when searching for users", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode("User deleted successfully"); err != nil {
http.Error(w, "Error encoding users to JSON", http.StatusInternalServerError)
return
}
}
user_service.go:
func DeleteUserCorrect(id string) error {
query := "DELETE FROM users WHERE id = $1"
_, err := database.DBConnection.Exec(query, id)
if err != nil {
return err
}
return nil
}
让我们调用端点并尝试注入 SQL 语句:
DELETE http://localhost:8080/users/correct?id='1'OR'1'='1' HTTP/1.1
content-type: application/json
我们收到错误提示并避免了 SQL 注入!
HTTP/1.1 500 Internal Server Error
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Wed, 27 Dec 2023 15:58:52 GMT
Content-Length: 28
Connection: close
Error when deleting users
正确删除:
GET http://localhost:8080/users/correct?id=1 HTTP/1.1
content-type: application/json
我们只删除我们想要删除的ID的用户。
"User deleted successfully"
最后考虑因素
本文介绍了如何模拟和避免 SQL 注入攻击。虽然有多种方法可以缓解这种攻击,但使用数据库驱动程序提供的资源通常是最简单、最安全的。不过,在调用驱动程序之前进行验证可以有效降低这种漏洞的风险。使用 ORM 也能显著降低 SQL 注入的概率。ORM 本身已经处理了这个问题,但仍然有可能发生。
存储库链接
项目仓库
请点击此处查看我博客上的文章。
文章来源:https://dev.to/wiliamvj/preventing-sql-injection-with-golang-41m5
