全栈杂谈第13期:什么是系统中的垂直越权

350 阅读11分钟

引言

随着互联网的快速发展,各类应用系统的安全性面临着越来越严峻的挑战。垂直越权作为一种常见的安全漏洞,可能导致严重的后果,如数据泄露、非法操作等。因此,深入理解垂直越权并采取有效的解决方案是每个开发者和安全从业者的重要任务。

垂直越权,也称为权限提升攻击,是指低权限用户通过某种手段获得高权限用户的权限,从而能够执行高权限用户才能执行的操作。例如,一个普通用户能够访问管理员才能访问的功能或数据。

垂直越权的危害包括数据泄露、非法操作和破坏系统稳定性。它可能导致低权限用户获取到敏感数据,执行高权限用户才能进行的操作,以及不当操作可能导致系统崩溃或出现异常情况。

危害

  1. 数据泄露:低权限用户可能获取到敏感数据,如用户的个人信息、财务数据等。
  2. 非法操作:可以执行高权限用户才能进行的操作,如修改系统配置、删除重要数据等。
  3. 破坏系统稳定性:不当的操作可能导致系统崩溃或出现异常情况。

常见场景

  1. 电商平台:普通用户可能通过某种方式访问管理员的订单管理界面,进行订单的修改或删除操作。
  2. 企业管理系统:员工可能获取到经理级别的权限,查看或修改员工绩效评估等敏感信息。
  3. 社交网络:用户可能突破自身权限,访问其他用户的私密信息。

常见解决方案

针对垂直越权的解决方案主要有以下几种:

  1. 接口级校验:对每个接口进行权限校验,确保只有拥有相应权限的用户才能访问。
  2. 网关级校验:在微服务架构中,通过统一的网关进行权限校验,集中管理权限规则。
  3. 接口签名:通过签名机制防止请求被伪造和篡改,增强安全性。
  4. 强化权限控制:实施细粒度的权限控制和角色基权限控制(RBAC),确保用户只能访问和操作其权限范围内的数据。
  5. 加密服务间通信:使用TLS/SSL对服务间通信进行加密,确保数据传输的安全性。
  6. 完善身份验证和授权机制:采用OAuth 2.0协议和JWT(JSON Web Tokens)进行用户认证和授权。
  7. 简化服务间依赖关系:通过服务拆分和隔离降低服务间的依赖关系,减少风险传播。
  8. 监控和审计:实施日志记录和实时监控,便于追踪和审计用户操作和服务调用。

解决方案实现

接口级校验解决方案

在每个接口中进行权限校验,确保只有具有相应权限的用户才能访问该接口。这种方式直接在业务逻辑层进行权限控制,能够有效地防止垂直越权攻击。

实现步骤
  1. 确定用户权限级别:为每个用户分配不同的权限级别,如普通用户、管理员等。
  2. 接口权限标注:在每个接口上标注所需的权限级别。
  3. 权限校验逻辑:在接口被调用时,检查调用者的权限级别是否满足接口的要求。
伪代码示例

Java示例

public class PermissionChecker {
    public boolean hasPermission(User user, String requiredPermission) {
        return user.getPermissions().contains(requiredPermission);
    }
}

@RestController
public class MyController {
    @Autowired
    private PermissionChecker permissionChecker;

    @GetMapping("/admin/data")
    public ResponseEntity<String> getAdminData(@AuthenticationPrincipal User user) {
        if (!permissionChecker.hasPermission(user, "ADMIN")) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Access Denied");
        }
        // 业务逻辑
        return ResponseEntity.ok("Admin Data");
    }

}

Go示例

type User struct {
    Permissions []string
}

func (u *User) HasPermission(requiredPermission string) bool {
    for _, permission := range u.Permissions {
        if permission == requiredPermission {
            return true
        }
    }
    return false
}

func getAdminData(w http.ResponseWriter, r *http.Request) {
    user := r.Context().Value("user").(*User)
    if !user.HasPermission("ADMIN") {
        http.Error(w, "Access Denied", http.StatusForbidden)
        return
    }
    // 业务逻辑
    w.Write([]byte("Admin Data"))
}

Python示例

class User:
    def __init__(self, permissions):
        self.permissions = permissions

    def has_permission(self, required_permission):
        return required_permission in self.permissions

def get_admin_data(request):
    user = request.user
    if not user.has_permission("ADMIN"):
        return HttpResponseForbidden("Access Denied")
    # 业务逻辑
    return HttpResponse("Admin Data")

优点:

  1. 精细化权限管理:通过为每个接口定制权限控制,我们能够实现对用户访问权限的精准管理,确保只有具备相应权限的用户才能访问特定的接口资源。
  2. 简洁明了的实现:权限校验逻辑直接嵌入到业务代码中,使得代码结构直观易懂,便于开发人员理解和实现,同时也方便新成员快速上手。

缺点:

  1. 代码冗余:由于每个接口都需要独立的权限校验逻辑,这可能导致代码的重复,增加代码的冗余度,影响代码的整洁性和可维护性。
  2. 维护挑战:随着业务的发展和权限规则的变更,需要在多个接口中同步更新权限控制逻辑,这增加了维护工作的复杂度和成本,尤其是在大型项目中,这种维护工作可能会变得尤为繁琐。

网关级校验解决方案

在网关层对请求进行权限校验,所有的请求都必须经过网关,网关根据用户的权限信息和请求的目标接口,决定是否允许该请求通过。

实现步骤
  1. 部署网关:可以使用开源的网关软件,如 Zuul、Kong 等,或者自己开发网关。
  2. 配置权限规则:在网关中配置用户权限与接口的对应关系。
  3. 权限校验逻辑:当请求到达网关时,网关根据配置的权限规则进行校验,如果请求合法,则转发到目标服务;如果不合法,则拒绝请求。
伪代码示例

Spring Cloud Gateway

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class TokenFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        
        // 简单权限校验逻辑
        if (token == null || !isValidToken(token)) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        
        return chain.filter(exchange);
    }
    
    private boolean isValidToken(String token) {
        // 模拟校验逻辑,比如判断Token是否匹配某个规则
        return "valid-token".equals(token);
    }

}

优点:

  1. 集中式权限治理:通过在网关层集中处理权限校验,可以实现权限规则的统一管理和维护,从而简化了权限控制的复杂性,并提高了管理效率。
  2. 高度可扩展性:网关层的权限校验机制支持灵活地添加新的权限规则和接口,无需对后端服务代码进行修改,这极大地增强了系统的可扩展性和适应性。

缺点:

  1. 性能考量:由于所有请求都必须通过网关进行权限校验,这可能会对系统性能产生一定影响,尤其是在高流量情况下,性能开销可能更为显著。
  2. 系统复杂度提升:引入网关层进行权限校验虽然带来了集中管理的好处,但同时也增加了系统的架构复杂度,需要额外的配置和管理,这可能会对系统稳定性和维护工作带来挑战。

接口签名解决方案

接口签名是通过对请求参数进行签名,确保请求的合法性和完整性。只有拥有正确签名的请求才能被服务端接受,从而防止非法用户伪造请求进行垂直越权攻击。

实现步骤
  1. 生成签名:客户端在发送请求之前,根据请求参数和密钥生成一个签名。
  2. 添加签名到请求:将生成的签名添加到请求的头部或参数中。
  3. 服务端校验签名:服务端接收到请求后,根据请求参数和密钥重新计算签名,并与请求中的签名进行比较。如果签名一致,则认为请求合法;如果不一致,则拒绝请求。
伪代码示例

Java示例

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.TreeMap;

public class SignatureUtil {
    public static String generateSignature(Map<String, String> params, String secretKey) throws NoSuchAlgorithmException {
        TreeMap<String, String> sortedParams = new TreeMap<>(params);
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
            sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
        }
        sb.append("secretKey=").append(secretKey);
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        byte[] hash = md.digest(sb.toString().getBytes());
        StringBuilder hexString = new StringBuilder();
        for (byte b : hash) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) hexString.append('0');
            hexString.append(hex);
        }
        return hexString.toString();
    }

    public static boolean verifySignature(Map<String, String> params, String secretKey, String receivedSignature) throws NoSuchAlgorithmException {
        String expectedSignature = generateSignature(params, secretKey);
        return expectedSignature.equals(receivedSignature);
    }

}

@RestController
public class MyController {
    private static final String SECRET_KEY = "mySecretKey";

    @PostMapping("/secure/data")
    public ResponseEntity<String> secureData(@RequestParam Map<String, String> params, @RequestHeader("X-Signature") String receivedSignature) {
        try {
            if (!SignatureUtil.verifySignature(params, SECRET_KEY, receivedSignature)) {
                return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Invalid Signature");
            }
            // 业务逻辑
            return ResponseEntity.ok("Data Processed");
        } catch (NoSuchAlgorithmException e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error processing signature");
        }
    }

}

Go示例

import (
    "crypto/sha256"
    "encoding/hex"
    "net/http"
    "sort"
    "strings"
)

func generateSignature(params map[string]string, secretKey string) string {
    keys := make([]string, 0, len(params))
    for k := range params {
        keys = append(keys, k)
    }
    sort.Strings(keys)

    var sb strings.Builder
    for _, k := range keys {
        sb.WriteString(k)
        sb.WriteString("=")
        sb.WriteString(params[k])
        sb.WriteString("&")
    }
    sb.WriteString("secretKey=")
    sb.WriteString(secretKey)
    
    hash := sha256.Sum256([]byte(sb.String()))
    return hex.EncodeToString(hash[:])

}

func verifySignature(params map[string]string, secretKey string, receivedSignature string) bool {
    expectedSignature := generateSignature(params, secretKey)
    return expectedSignature == receivedSignature
}

func secureData(w http.ResponseWriter, r *http.Request) {
    secretKey := "mySecretKey"
    receivedSignature := r.Header.Get("X-Signature")
    params := r.URL.Query()

    if !verifySignature(params, secretKey, receivedSignature) {
        http.Error(w, "Invalid Signature", http.StatusForbidden)
        return
    }
    // 业务逻辑
    w.Write([]byte("Data Processed"))

}

Python示例

import hashlib
import hmac
from flask import Flask, request, jsonify

app = Flask(__name__)

def generate_signature(params, secret_key):
    sorted_params = sorted(params.items())
    query_string = '&'.join(f"{k}={v}" for k, v in sorted_params)
    query_string += f"&secretKey={secret_key}"
    return hmac.new(secret_key.encode(), query_string.encode(), hashlib.sha256).hexdigest()

def verify_signature(params, secret_key, received_signature):
    expected_signature = generate_signature(params, secret_key)
    return hmac.compare_digest(expected_signature, received_signature)

@app.route('/secure/data', methods=['POST'])
def secure_data():
    secret_key = "mySecretKey"
    received_signature = request.headers.get('X-Signature')
    params = request.args

    if not verify_signature(params, secret_key, received_signature):
        return jsonify({"error": "Invalid Signature"}), 403
    # 业务逻辑
    return jsonify({"message": "Data Processed"})

if __name__ == '__main__':
    app.run()

优点:

  1. 高安全性:接口签名通过加密技术确保请求的合法性和完整性,有效防止请求被非法伪造或篡改。
  2. 业务逻辑解耦:接口签名的实现独立于业务逻辑,允许在不同系统和接口中复用,提高了代码的通用性和灵活性。

缺点:

  1. 密钥管理挑战:需要严格管理签名密钥,以确保系统的安全性。一旦密钥泄露,系统可能会面临严重的安全风险。
  2. 性能考量:签名的生成和验证过程可能会引入额外的性能开销,尤其是在高并发请求的场景下。

三种解决方案的比较

  1. 安全性对比
    • 接口级校验:提供了业务逻辑层面的精细权限控制,但若存在代码漏洞,可能被攻击者绕过。
    • 网关级校验:集中管理权限规则,有效拦截非法请求,但一旦网关被攻破,可能危及整个系统的安全。
    • 接口签名:通过签名机制确保请求的合法性和完整性,安全性较高,但密钥管理不善可能引发安全问题。
  2. 性能对比
    • 接口级校验:每个接口都需要进行权限校验,可能在高并发情况下带来显著的性能开销。
    • 网关级校验:所有请求均需通过网关校验,可能对性能产生较大影响。
    • 接口签名:生成和校验签名会有一定的性能开销,但相对较小。
  3. 可维护性对比
    • 接口级校验:权限规则变更时,需要在多个接口中修改,维护成本较高。
    • 网关级校验:权限规则集中管理,维护相对容易。
    • 接口签名:密钥管理和签名算法的维护独立于业务逻辑,减少了对业务代码的影响。

实际应用中的选择

在实际应用中,选择合适的解决方案应考虑以下因素:

  1. 系统架构:对于采用微服务架构且有统一网关的系统,网关级校验有利于集中管理权限规则。若业务逻辑复杂,需精细权限控制,可结合接口级校验和接口签名以增强安全性。
  2. 性能要求:若系统对性能要求极高,接口签名因其较小的性能开销成为首选。若性能非主要考量,接口级校验或网关级校验可提供更好的安全性和可维护性。
  3. 安全需求:若系统对安全性要求极高,可同时采用接口级校验、网关级校验和接口签名,构建多层防护体系。若需防止请求伪造和篡改,接口签名是合适的选择。

欢迎关注公众号:“全栈开发指南针”

这里是技术潮流的风向标,也是你代码旅程的导航仪!🚀

Let’s code and have fun! 🎉