如何在 Python 和 Flask 中使用 IP API 查找地理位置?
Golang 损坏对象 等级授权指南: 示例和预防
根据OWASP 2021 年十大漏洞,Web API 中最常见的漏洞是访问控制中断。此安全问题包含多个问题,但最大的问题是对象级授权中断。当应用程序在提供对特权资源的访问权限之前未正确验证用户权限时,就会发生此问题。这是一个严重的安全漏洞,许多应用程序都无法解决。
我们将研究 golang 应用程序中损坏的对象级授权 (BOLA) 问题的示例以及如何修复和预防这些问题。
对象级授权失效
对象级授权可确保用户只能访问他们拥有适当权利的对象。虽然第三方应用程序和服务通常会实现身份验证,但应用程序需要负责授权访问其内部数据。当应用程序无法做到这一点时,就会出现 BOLA。根据问题的性质,它允许用户读取、更改、删除或创建他们不应该访问的数据。
攻击者会快速利用 Web API 中新发现的 BOLA 问题,因为只需几个简单的脚本即可实现。因此,BOLA 意味着数据丢失或泄露的可能性非常高。
我们来看一个例子。
BOLA 示例
想想用于管理网站用户的典型创建读取更新删除 (CRUD) API。
用户如下所示:
{
"id":"1",
"lastname":"Doe",
"firstname":"John",
"dept":"Janitorial",
"email":"jdoe@demo.com"
}
它有五个端点:
- 使用 JSON 有效负载的 POST /users/ 来添加用户。
- GET /users/{ID} 来检索用户。
- GET /users 检索所有用户的列表。
- PUT /users/{ID} 更新用户。
- DELETE /users/{ID} 删除用户。
这是 CRUD API 的标准实现。潜在的问题是,如果您知道用户的 ID,则可以检索、更新和删除用户。
如果 API 仅检查用户是否有效且经过身份验证,但未能确认他们是否应该能够访问用户 ID,则存在 BOLA 漏洞。更糟糕的是,如果您有 BOLA 并且对象 ID 是连续的,攻击者可以利用此漏洞并访问您的所有对象。
Golang 破坏对象级授权
让我们看一下上面作为 golang web 服务实现的示例。我们将看到损坏的对象级授权的实际操作,然后我们将修复它。
该服务使用Gorilla 来路由 API 请求,因此主函数将请求映射到处理程序并设置监听器。
func main() {
r := mux.NewRouter()
usersR := r.PathPrefix("/users").Subrouter()
usersR.Path("").Methods(http.MethodGet).HandlerFunc(getAllUsers)
usersR.Path("").Methods(http.MethodPost).HandlerFunc(createUser)
usersR.Path("/{id}").Methods(http.MethodGet).HandlerFunc(getUserByID)
usersR.Path("/{id}").Methods(http.MethodPut).HandlerFunc(updateUser)
usersR.Path("/{id}").Methods(http.MethodDelete).HandlerFunc(deleteUser)
fmt.Println("Start listening")
fmt.Println(http.ListenAndServe(":8080", r))
}
因此,我们可以将注意力集中在 golang 破坏的对象级授权上,我们将在示例代码中采取一些捷径。
我们不会检查代码来创建、验证和管理安全的用户会话,而是使用单个模拟来确认当前会话是否有效。
此外,我们不会查看 SQL 或 NoSQL 代码,而是使用模拟来获取和设置用户信息。
查看用户
因此,这是getUserById处理程序。
func getUserByID(w http.ResponseWriter, r *http.Request) {
valid, err := checkSession(r)
if valid == false {
fmt.Println(err)
http.Error(w, "Invalid user session!", http.StatusMethodNotAllowed)
return
}
id := mux.Vars(r)["id"]
u, err := getUserFromStore(id)
if err != nil {
http.Error(w, "Error retrieving user", http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(&u); err != nil {
fmt.Println(err)
http.Error(w, "Error encoding response object", http.StatusInternalServerError)
}
}
因此我们可以使用 cURL 查看 BOLA 漏洞,我们将使用单个标头来模拟有效会话。
egoebelbecker@genosha-2 ~ % curl -H "Session_ID: rekcebleboeg" localhost:8080/users/1
{"id":"1","lastname":"Doe","firstname":"John","dept":"","email":"jdoe@demo.com"}
由于此 GET 处理程序仅验证请求用户是否具有有效会话,因此任何能够创建会话的人都可以通过 Id 请求用户。
由于我们已经两次描述了这个错误,并且我们正在查看调用名为 checkSession()的方法的代码,因此这个错误似乎很明显。但情况并非总是如此,这是一个常见的设计错误。许多 API 被设计为在 Web GUI 和移动应用程序之间共享。当应用程序缺少允许您查看其他人信息的 GUI 元素时,它们会隐式执行对象级授权。有时它们甚至被构造为只有一个应用程序可以执行某些功能。
但攻击者会查看 Web 源代码并逆向工程 API,可能只需几分钟。此 API 不会检查访问权限是否正确,并且具有连续的用户 ID。情况很糟糕。
查看所有用户
假设管理用户拥有不同的 Web 界面。他们可以查看所有用户。
func getAllUsers(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
valid, err := checkSession(r)
if valid == false {
fmt.Println(err)
http.Error(w, "Invalid user session!", http.StatusMethodNotAllowed)
return
}
var users []User
for key := range store.Keys(nil) {
u, err := getUserFromStore(key)
if err != nil {
http.Error(w, "Error retrieving user", http.StatusInternalServerError)
return
}
users = append(users, u)
}
if err := json.NewEncoder(w).Encode(users); err != nil {
fmt.Println(err)
http.Error(w, "Error encoding response object", http.StatusInternalServerError)
}
}
因此,攻击者不需要使用连续的 ID 来获取整个用户数据库。
这是那个请求。(添加了一些格式。)
egoebelbecker@genosha-2 ~ % curl -H "Session_ID: rekcebleboeg" localhost:8080/users
[{"id":"1","lastname":"Doe","firstname":"John","dept":"HR","email":"jdoe@demo.com"},
{"id":"2","lastname":"Smith","firstname":"Jane","dept":"Dev","email":"jsmith@demo.com"},
{"id":"3","lastname":"Neuman","firstname":"Alfred E.","dept":"CEO","email":"whatme@worry.com"},
{"id":"4","lastname":"Goebelbecker","firstname":"Eric","dept":"Janitorial","email":"noreply@demo.com"}]
再次,应用程序设计人员依赖客户端应用程序中的限制来阻止用户看到他们不应该看到的信息。
修复 Golang BOLA
那么我们该如何解决这些问题呢?有几种不同的方法。让我们简要地看一下每种方法。
错误的答案
一个答案是从 API URL 中删除用户 ID。
因此这五个端点可能看起来像这样:
- 使用 JSON 有效负载的 POST /users/ 来添加用户。
- GET /users/ 以包含 Id 的 Header 或 JSON 负载检索用户。
- GET /users 检索所有用户的列表。
- PUT /users/ 更新用户。
- DELETE /users/ 删除具有包含 Id 的 Header 或 JSON 有效负载的用户。
这只会改变两个端点,而所有漏洞都保留了下来。这可能会让经验丰富的黑客更难利用该 API。在对 cURL 进行几分钟的试验后,它可能不会让熟练的攻击者感到困扰。
还有更好的解决方案。
添加对象级别授权
最好且唯一有效的解决方案是添加代码来验证用户是否有权访问他们想要创建、读取、更新或删除的对象。
示例应用程序已有一个方法来验证会话是否有效。为了遵守单一职责原则,让我们添加一个新方法来检查用户是否可以执行他们要求的操作。
有很多方法可以做到这一点。有些方法比其他方法更好,有些方法适合在关于 BOLA 的简短博客文章中介绍,并且不是生产代码。
首先,让我们定义用户想要做什么以及他们是否可以做。枚举非常适合此目的。
定义操作和授权
type Action int64
const (
Read Action = 0
Write = 1
Update = 2
Delete = 3
Create = 4
List = 5
)
type Authorization int64
const (
Denied Authorization = 0
Allowed = 1
)
现在,我们可以编写一种方法来询问用户是否可以执行他们要求的操作。
func checkAuthorization(requester string, action Action, target string) (auth Authorization) {
switch action {
case Read:
if requester == target || requester == admin {
return Allowed
}
break;
case Create:
case Update:
case Delete:
case Write:
case List:
if requester == admin {
return Allowed
}
}
return Denied
}
这是对象级授权的基本示例。该方法允许用户读取其信息,并且仅允许管理员用户执行其他操作。但是,由于我们对请求的操作使用了枚举,因此很容易为不同的操作添加不同级别的权限。因此,部门主管可以修改其团队成员的记录,或者人力资源团队可以添加地址信息等。
将其添加到 API
我们来看看新的getUserbyID:
func getUserByID(w http.ResponseWriter, r *http.Request) {
valid, user, err := checkSession(r)
if valid == false {
fmt.Println(err)
http.Error(w, "Invalid user session!", http.StatusMethodNotAllowed)
return
}
id := mux.Vars(r)["id"]
var auth = checkAuthorization(user, Read, id)
if auth == Allowed {
u, err := getUserFromStore(id)
if err != nil {
http.Error(w, "Error retrieving user", http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(&u); err != nil {
fmt.Println(err)
http.Error(w, "Error encoding response object", http.StatusInternalServerError)
}
} else {
http.Error(w, "Not permitted", http.StatusMethodNotAllowed)
return
}
}
我们必须修改checkSession以返回用户 ID,这样我们才能使用它来检查权限。如果通过,该方法将像以前一样继续。如果没有通过,它会向请求应用程序返回错误。
现在,getAllusers看起来像这样:
func getAllUsers(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
valid, user, err := checkSession(r)
if valid == false {
fmt.Println(err)
http.Error(w, "Invalid user session!", http.StatusMethodNotAllowed)
return
}
var auth = checkAuthorization(user, List, "")
if auth == Allowed {
var users []User
for key := range store.Keys(nil) {
u, err := getUserFromStore(key)
if err != nil {
http.Error(w, "Error retrieving user", http.StatusInternalServerError)
return
}
users = append(users, u)
}
if err := json.NewEncoder(w).Encode(users); err != nil {
fmt.Println(err)
http.Error(w, "Error encoding response object", http.StatusInternalServerError)
}
} else {
http.Error(w, "Not permitted", http.StatusMethodNotAllowed)
return
}
}
由于我们尝试使用 List 操作,因此 我们不需要将目标用户传递给checkAuthorization 。
我们将checkAuthorization添加到此应用程序中的其他三个方法中以完成这项工作,并且我们拥有基本的对象级授权。随着时间的推移,它可以发展成为一个更复杂的示例,具有多层授权实例,是一个粗略的基于角色的系统。
附加
虽然我们已经修补了这个简单 API 中的大漏洞,但我们可以采取更多措施使其更加强大:将用户 ID 从数字更改为更难猜测的非连续格式,例如 UUID。虽然这不会解决代码中的任何安全问题,但它确实使 API 成为不那么有吸引力的目标。
文章来源:Golang Broken Object Level Authorization Guide: Examples and Prevention