【网关鉴权】header修改不生效问题

关键字:#SpringCloud、#header、#MultiValueMap、#鉴权

1 问题背景

安全团队反馈:直接给header的real_user_id字段传其他用户的user_id,即可绕过鉴权,达到类似劫持cookie的效果。

从网关鉴权开始排查。

2 网关鉴权流程

网关层鉴权结果写入header的real_user_id字段,鉴权不通过的写-1。

下游服务通过real_user_id字段获取真实的用户id,若为-1则视为登录鉴权不通过。

下游业务层使用的user_id取的并非用户原始传入的user_id字段,而是网关鉴权后往header里塞的一个新的字段real_user_id。

这个real_user_id字段是仅供内部使用,外部不应该知道这一字段的存在。

整个请求流程中,real_user_id的值应该仅在网关鉴权完成后被赋值。

// 鉴权        
Boolean authResult = auth(user_id, token);
// 鉴权结果
Long realUserId = authResult ? user_id : -1;
// 修改header
ServerHttpRequest verifiedRequest = exchange.getRequest().mutate()
         .headers(httpHeaders -> httpHeaders.add(HEADER_REAL_USER_ID, realUserId))
         .headers(httpHeaders -> httpHeaders.add(HEADER_AUTH_STATUS, authStatus))
         .build();

3 header的底层存储结构:MultiValueMap

仅从上面的代码看,即使内部字段遭到泄漏,在请求时赋值的real_user_id,也应该在鉴权之后被鉴权结果覆盖,而事实上并非如此。

查看Spring的header的源码,发现header底层的存储是基于MultiValueMap的

虽然名字是map,但这种结构允许map内存在重复的<key, value>

package org.springframework.http;

public class HttpHeaders implements MultiValueMap<String, String>, Serializable {
    // ...
	 final MultiValueMap<String, String> headers;
    // ...
    // 添加元素
    public void add(String headerName, @Nullable String headerValue) {
        this.headers.add(headerName, headerValue);
    }
}

翻到最底层,可以发现,MultiValueMap的本质是Map<String, List>,当一个key对应多个value时,会将value存进一个List。

public class MultiValueMapAdapter<K, V> implements MultiValueMap<K, V>, Serializable {
    private final Map<K, List<V>> targetMap;
		
    // 获取key对应的首个元素
    @Nullable
    public V getFirst(K key) {
        List<V> values = (List)this.targetMap.get(key);
        return values != null && !values.isEmpty() ? values.get(0) : null;
    }

    // 添加元素
    public void add(K key, @Nullable V value) {
        List<V> values = (List)this.targetMap.computeIfAbsent(key, (k) -> {
            return new ArrayList(1);
        });
        values.add(value);
    }
}

所以回到实际的问题,经过请求传参real_user_id,和鉴权后添加real_user_id两次赋值后,此时的real_user_id对应的key-value应该是<real_user_id, [user_id_a, -1]>

再把目光转向业务层的服务,从header取值的逻辑基本都是取key对应的第一个值。

@Nullable
public String getHeader(String name) {
    List<String> value = (List)this.headers.get(name);
    // 取第一个值
    return CollectionUtils.isEmpty(value) ? null : (String)value.get(0);
}

看到这里终于真相大白了,传参时传入的real_user_id会被一直捅到业务层,并且让鉴权结果失效。

4 修复

鉴权完成后,往header赋值之前,先清除可能的real_user_id传值。

ServerHttpRequest verifiedRequest = exchange.getRequest().mutate()
        // 塞之前先清一波 防止恶意传参
        .headers(httpHeaders -> httpHeaders.remove(REAL_USER_ID))
        .headers(httpHeaders -> httpHeaders.add(REAL_USER_ID, realUserId))
        .headers(httpHeaders -> httpHeaders.add(HEADER_AUTH_STATUS, authStatus))
        .build();
posted @ 2024-06-14 17:15  CofJus  阅读(5)  评论(0编辑  收藏  举报