后端:任何客户端的东西都不可信任

简介: 有这么一个问题,许多做业务开发的同学往往一点点安全意识都没有。如果只是用一些所谓的渗透服务浅层次地做一下扫描和渗透,而不在代码和逻辑层面做进一步分析的话,能够发现的安全问题非常有限。要做好安全,还是要靠程序员和产品经理点点滴滴的意识

有这么一个问题,许多做业务开发的同学往往一点点安全意识都没有。如果只是用一些所谓的渗透服务浅层次地做一下扫描和渗透,而不在代码和逻辑层面做进一步分析的话,能够发现的安全问题非常有限。要做好安全,还是要靠程序员和产品经理点点滴滴的意识



   对于 HTTP 请求,我们应该在脑子里有一个根深蒂固的概念,那就是任何客户端传过来的数据都是不能直接信任的。客户端传给服务端的数据只是信息收集,数据需要经过有效性验证、权限验证等后才能使用,并且这些数据只能认为是用户操作的意图,不能直接代表数据当前的状态


   举个例子,我们打游戏的时候,客户端发给服务端的只是用户的操作,比如移动了多少位置,由服务端根据用户当前的状态来设置新的位置再返回给客户端。为了防止作弊,不会由客户端直接告诉服务端用户当前的位置。因此,客户端发给服务端的指令,代表的只是操作指令,并不能直接决定用户的状态,对于状态改变的计算在服务端。而网络不好时,我们往往会遇到走了 一段距离又被服务端拉回来的现象,就是因为有指令丢失,客户端使用服务端计算的实际位置修正了客户端玩家的位置

一、客户端的计算不可信

比如在电商下单这个场景中,我们可能会暴露这么一个 /order 的 POST 接口给客户端,让客户端直接把组装后的订单信息 Order 传给服务端

@PostMapping("/order")
public void wrong(@RequestBody Order order) {
    this.createOrder(order);
}

订单信息 Order 可能包括商品 ID、商品价格、数量、商品总价


@Data
public class Order {
    private long itemId; //商品ID
    private BigDecimal itemPrice; //商品价格
    private int quantity; //商品数量
    private BigDecimal itemTotalPrice; //商品总价
}

用户下单时,客户端肯定会有商品价格等信息,也会计算出总价给用户确认,但这些信息只能用于呈现和核对。即使客户端把商品价格传了过来,服务端也一定要去数据库获取商品的价格,重新计算最终的订单价格。不然很可能会被黑客利用,商品总价被恶意修改为比较低的价格


因此,我们真正可以信赖并且能直接使用的只有商品的id和数量,然后重新计算总价。如果总价和客户端传过来的商品总价不一致,可以给客户端一个友好提示,让用户重新下单。


还有一种方法,客户端只给服务端传商品id和数量进行下单操作,下单成功后,服务端处理完成并返回诸如商品单价、总价等信息给客户端。此时,客户端可以进行一次判断,如果和之前客户端的数据不一致的话,给予用户提示,用户确认没问题后再进入支付阶段。


通过这个案例可以发现,在处理客户端提交过来的数据时,服务端需要明确区分,哪些数据可信任,哪些数据不可信任。

二、客户端提交的参数需要校验

对于客户端的数据,还容易忽略的一点是,误以为客户端的数据来源是服务端,客户端就不可能提交异常数据。下面有个案例


有一个用户注册页面要让用户选择所在国家,我们会把服务端支持的国家列表返回给页面,供用户选择。现在我们的注册只支持中国、美国和英国三个国家,并不对其他国家开放,因此从数据库中筛选了 id<4 的国家返回给页面进行填充。页面拿到数据进行渲染后,肯定只有三个可选项。


但是,页面是给普通用户使用的,而黑客不会在乎页面显示什么,完全有可能尝试给服务端返回页面上没显示的其他国家 ID。如果直接信任客户端传来的国家 ID 的话,很可能会把用户注册功能开放给其他国家的用户。


即使我们知道参数的内容本来就是服务端返回的,也需要对参数进行校验。因为接口不一定要通过浏览器请求,还有很多其他工具直接请求接口。


所以应对措施是,对参数进行校验,如下:


@PostMapping("/right")
@ResponseBody
public String right(@RequestParam("countryId") int countryId) {
    if (countryId < 1 || countryId > 3)
        throw new RuntimeException("非法参数");
    return allCountries.get(countryId).getName();
}

或者想要优雅一点,使用 Spring Validation 采用注解的方式进行参数校验(我个人一直用的这种方式,优雅永不过时~):


@Validated
@RestController
public class TrustClientParameterController {
    @PostMapping("/better")
    public String better(
            @Min(value = 1, message = "非法参数")
            @Max(value = 3, message = "非法参数") int countryId) {
        return allCountries.get(countryId).getName();
    }
}

客户端提交的参数需要校验的问题,可以引申出一个更容易忽略的点是,我们可能会把一些服务端的数据暂存在网页的隐藏域中(比如浏览器的localstorage),这样下次页面提交的时候可以把相关数据再传给服务端。虽然用户通过网页界面的操作无法修改这些数据,但这些数据对于 HTTP 请求来说就是普通数据,完全可以随时修改为任意值。所以,服务端在使用这些数据的时候,也同样要特别小心。


三、不能信任请求头里面的任何内容

一个比较常见的需求是,为了防刷,我们需要判断用户的唯一性。比如,针对未注册的新用户发送一些小奖品,并且不希望相同用户多次获得奖品。而未注册的用户因为没有登录过所以没有用户标识,我们可能会想到根据请求的 IP 地址,来判断用户是否已经领过奖品。


下面的这段测试代码。通过一个 HashSet 模拟已发放过奖品的 IP 名单,每次领取奖品后把 IP 地址加入这个名单中。IP 地址的获取方式是:优先通过 X-Forwarded-For 请求头来获取,如果没有的话再通过 HttpServletRequest 的 getRemoteAddr 方法来获取。


@RequestMapping("trustclientip")
@RestController
public class TrustClientIpController {
    HashSet<String> activityLimit = new HashSet<>();
    @GetMapping("test")
    public String test(HttpServletRequest request) {
        String ip = getClientIp(request);
        if (activityLimit.contains(ip)) {
            return "您已经领取过奖品";
        } else {
            activityLimit.add(ip);
            return "奖品领取成功";
        }
    }
    private String getClientIp(HttpServletRequest request) {
        String xff = request.getHeader("X-Forwarded-For");
        if (xff == null) {
            return request.getRemoteAddr();
        } else {
            return xff.contains(",") ? xff.split(",")[0] : xff;
        }
    }
}


这么做是因为,通常我们的应用之前都部署了反向代理或负载均衡器,remoteAddr 获得的只能是代理的 IP 地址,而不是访问用户实际的 IP。这不符合我们的需求,因为反向代理在转发请求时,通常会把用户真实 IP 放入 X-Forwarded-For 这个请求头中。


但是这种过于依赖 X-Forwarded-For 请求头来判断用户唯一性的实现方式,是有问题的:完全可以通过 cURL 类似的工具来模拟请求,随意篡改头的内容:


curl http://localhost:8888/trustclientip/test -H "X-Forwarded-For:183.84.18.71, 10.253.15.1"

因此,IP 地址或者请求头里的任何信息,包括 Cookie 中的信息、Referer,只能用作参考,不能用作重要逻辑判断的依据。而对于类似这个案例唯一性的判断需求,更好的做法是,让用户进行登录或三方授权登录(比如微信),拿到用户标识来做唯一性判断。


四、用户标识不能从客户端获取

业务代码非常容易犯错的一个地方是,使用了客户端传给服务端的用户 ID,类似这样:


@GetMapping("wrong")

public String wrong(@RequestParam("userId") Long userId) {

   return "当前用户Id:" + userId;

}

如果接口面向内部服务,由服务调用方传入用户 ID 没什么不合理,但是这样的接口不能直接开放给客户端或 H5 使用。


如果你的接口直面用户(比如给客户端或 H5 页面调用),那么一定需要用户先登录才能使用。登录后用户标识保存在服务端,接口需要从服务端获取。比如使用session存储、使用redis存储。


最后,安全问题是木桶效应,整个系统的安全等级取决于安全性最薄弱的那个模块。在写业务代码的时候,要从我做起,建立最基本的安全意识,从源头杜绝低级安全问题


相关文章
|
2月前
|
消息中间件 运维 负载均衡
负载均衡中后端连了三个rabbitmq,如果挂了一个,客户端连接mq会变慢吗
在负载均衡中使用三个 RabbitMQ 实例,如果其中一个实例发生故障,可能会影响客户端连接到 RabbitMQ 的性能。具体影响取决于负载均衡的配置和客户端的实现方式。 如果负载均衡器能够及时检测到故障的 RabbitMQ 实例并将流量路由到正常的实例,那么客户端连接的性能影响可能较小。但如果负载均衡器不能迅速切换流量或者客户端实现不支持及时的连接故障转移,那么可能会导致客户端连接的延迟或失败。 在设计这样的架构时,有一些考虑因素: 1. **健康检查和故障切换:** 确保负载均衡器能够定期检查 RabbitMQ 实例的健康状态,并在出现故障时快速将流量切换到其他正常的实例。 2.
|
NoSQL Linux Redis
docker 安装redis 配置文件 设置密码 后端启动 进入客户端
docker 安装redis 配置文件 设置密码 后端启动 进入客户端
503 0
docker 安装redis 配置文件 设置密码 后端启动 进入客户端
|
移动开发 Python 网络协议
Python后端(一)——客户端/服务端
网址组成(四部分)     协议      http, https(https 是加密的http)     主机      g.cn zhihu.com之类的网址     端口      HTTP 协议默认是 80,因此一般不用填写     路径      下面的「/」和「/question/31838184」都是路径 http://www.
1491 0
|
负载均衡 应用服务中间件 nginx
nginx做反向负载均衡,后端服务器获取真实客户端ip
nginx增加header配置 server { listen 80; server_name admin.
1399 0
|
负载均衡 应用服务中间件 nginx
nginx做反向负载均衡,后端服务器获取真实客户端ip(转)
首先,在前端nginx上需要做如下配置: location / proxy_set_hearder host                $host; proxy_set_header X-forwarded-for $proxy_add_x_forwarded_for; prox...
1770 0
|
5天前
|
敏捷开发 存储 运维
深入理解后端开发中的微服务架构
在现代软件开发领域,微服务架构已经成为一种重要的设计模式。它通过将复杂的应用程序分解为一套小的、独立的服务来促进敏捷开发和快速迭代。本文旨在探索微服务架构的核心概念、优势与挑战,并结合具体案例分析其在实际应用中的表现。我们将从服务划分策略、通信机制,到数据一致性保障等方面,逐一剖析微服务架构的实施细节。此外,文章还将讨论微服务架构与传统单体架构的区别,以及如何平滑地从传统架构迁移至微服务架构。
|
10天前
|
Kubernetes 持续交付 Docker
现代后端开发中的微服务架构与容器化技术
本文探讨了现代后端开发中微服务架构与容器化技术的重要性和应用。微服务架构通过服务的拆分和独立部署提升了系统的灵活性和可维护性,而容器化技术则为微服务的快速部署和管理提供了解决方案。文章深入分析了微服务架构的优势、挑战以及如何利用容器化技术来支持微服务架构的实现。最后,通过实际案例展示了微服务与容器化技术在提升应用开发效率和系统稳定性方面的应用实践。【7月更文挑战第5天】
|
10天前
|
存储 缓存 负载均衡
高效后端开发中的架构设计与优化策略
在当今快速发展的技术环境中,高效的后端开发不仅仅依赖于编程技能,更需要精心设计的架构和优化策略。本文探讨了如何通过合理的架构设计和优化策略,提升后端系统的性能和可维护性,以应对复杂的业务需求和大规模的用户访问。【7月更文挑战第5天】
19 1
|
11天前
|
数据管理 物联网 开发者
现代化后端开发中的微服务架构设计与实现
在当今快速发展的软件开发领域,微服务架构已成为构建高效、可扩展和灵活的后端系统的重要方式。本文将探讨微服务架构的设计原则、实现方法以及应用场景,帮助开发者理解如何在项目中成功应用微服务。【7月更文挑战第4天】
25 2
|
2天前
|
Kubernetes 监控 Docker
现代后端开发中的微服务架构与容器化技术
传统的单体应用架构在面对现代大规模应用需求时已显不足,微服务架构及其伴随的容器化技术因其灵活性和可伸缩性成为了主流选择。本文探讨了微服务架构的优势及其与传统架构的对比,详细分析了容器化技术如何支持微服务的部署与管理,以及实际应用中的最佳实践。 【7月更文挑战第13天】
6 2

热门文章

最新文章