发布于 2026-01-06 5 阅读
0

使用 Golang 防止 SQL 注入

使用 Golang 防止 SQL 注入

我们将涵盖哪些内容?

SQL注入是攻击系统最常用的技术之一,我们可以利用它在易受攻击的端点上执行恶意SQL,从而操纵数据库。虽然已经有很多方法可以缓解这种攻击,但如果开发人员不注意,仍然可能留下漏洞。

大多数 ORM 已经能够抑制这种类型的攻击,但在 Go 语言中,不使用 ORM 的情况非常普遍,如果处理不当,这种漏洞发生的几率会更大。

SQL注入的工作原理

SQL 注入通常发生在提供了允许传递参数的过滤器,但这些参数没有被正确处理的情况下,例如:

GET http://localhost:8080/users?id=1 HTTP/1.1
Enter fullscreen mode Exit fullscreen mode

此端点通过 ID 搜索用户,但如果处理不当,可能会执行 SQL 注入攻击。

GET http://localhost:8080/users?id='1'OR'1'='1' HTTP/1.1
Enter fullscreen mode Exit fullscreen mode

'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
  }
Enter fullscreen mode Exit fullscreen mode

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()
  }
Enter fullscreen mode Exit fullscreen mode

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
  }
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
Enter fullscreen mode Exit fullscreen mode

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
  }
Enter fullscreen mode Exit fullscreen mode

在执行查询的服务中,我们直接传递 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
Enter fullscreen mode Exit fullscreen mode

我们收到了预期的回报:

[
  {
    "id": 1,
    "name": "John Doe",
    "email": "john.doe@example.com"
  }
]
Enter fullscreen mode Exit fullscreen mode

现在,让我们注入 SQL 语句:

GET http://localhost:8080/users?id='1'OR'1'='1' HTTP/1.1
content-type: application/json
Enter fullscreen mode Exit fullscreen mode

已接收所有用户:

[
  {
    "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"
  }
]
Enter fullscreen mode Exit fullscreen mode

由于注入的 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
    }
  }
Enter fullscreen mode Exit fullscreen mode

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
  }
Enter fullscreen mode Exit fullscreen mode

我们使用相同的逻辑按 ID 删除用户:

DELETE http://localhost:8080/users?id=1 HTTP/1.1
content-type: application/json
Enter fullscreen mode Exit fullscreen mode

调用此端点后,用户id = 1将被删除,现在让我们注入 SQL 语句:

DELETE http://localhost:8080/users?id='1'OR'1'='1' HTTP/1.1
content-type: application/json
Enter fullscreen mode Exit fullscreen mode

当使用注入的 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
    }
  }
Enter fullscreen mode Exit fullscreen mode

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
  }
Enter fullscreen mode Exit fullscreen mode

然而,现在我们不id直接传递参数,而是传递一个标记,该标记$1表明该位置将有一个参数,然后将其传递给查询。Query(query, id)这样,驱动程序就已经执行了我们所谓的“预处理语句”或“清理”、“水合”(您可以随意称呼它),这种方法有助于将查询逻辑与数据输入分离,从而提高数据库操作的安全性和完整性。

让我们调用端点并尝试注入 SQL 语句:

GET http://localhost:8080/users/correct?id='1'OR'1'='1' HTTP/1.1
content-type: application/json
Enter fullscreen mode Exit fullscreen mode

我们收到错误提示并避免了 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
Enter fullscreen mode Exit fullscreen mode

搜索正确:

GET http://localhost:8080/users/correct?id=2 HTTP/1.1
content-type: application/json
Enter fullscreen mode Exit fullscreen mode

现在我们能够正确接收用户数据了。

[
  {
    "id": 2,
    "name": "Bob",
    "email": "bob@example.com"
  }
]
Enter fullscreen mode Exit fullscreen mode

删除用户

让我们使用与搜索用户时相同的解决方案。

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
    }
  }
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

让我们调用端点并尝试注入 SQL 语句:

DELETE http://localhost:8080/users/correct?id='1'OR'1'='1' HTTP/1.1
content-type: application/json
Enter fullscreen mode Exit fullscreen mode

我们收到错误提示并避免了 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
Enter fullscreen mode Exit fullscreen mode

正确删除:

GET http://localhost:8080/users/correct?id=1 HTTP/1.1
content-type: application/json
Enter fullscreen mode Exit fullscreen mode

我们只删除我们想要删除的ID的用户。

"User deleted successfully"
Enter fullscreen mode Exit fullscreen mode

最后考虑因素

本文介绍了如何模拟和避免 SQL 注入攻击。虽然有多种方法可以缓解这种攻击,但使用数据库驱动程序提供的资源通常是最简单、最安全的。不过,在调用驱动程序之前进行验证可以有效降低这种漏洞的风险。使用 ORM 也能显著降低 SQL 注入的概率。ORM 本身已经处理了这个问题,但仍然有可能发生。

存储库链接

项目仓库

请点击此处查看我博客上的文章。

地鼠积分

文章来源:https://dev.to/wiliamvj/preventing-sql-injection-with-golang-41m5