发布于12月5日12月5日 # 简介 在Spring框架中,为了保证组件之间的依赖关系得到正确的管理和维护,推荐使用依赖注入(Dependency Injection,DI)。依赖注入是一种设计模式,它允许程序在运行时自动为类的成员变量赋值,而不是让程序员直接在代码中硬编码这些依赖关系。 使用Spring的依赖注入有以下好处: 1\.松散耦合:依赖注入可以保持各个组件之间的低耦合,因为组件不再负责创建或查找依赖对象。这有利于模块化开发和单元测试。 2\.可扩展性:由于组件彼此解耦,因此添加新功能或替换现有组件变得更加容易。 3\.易于管理:Spring容器可以集中管理所有组件实例以及它们之间的依赖关系,从而简化应用程序的配置和管理。 在Spring中,依赖注入可以通过以下方式进行: 1\.构造函数注入:通过构造函数传递依赖对象的实例。 ```` @服务 公共类我的服务{ 私有最终MyRepository 存储库; @Autowired 公共MyService(MyRepository存储库){ this.repository=存储库; } } ```` 2\. Setter注入:通过setting方法注入依赖对象的实例。 ```` @服务 公共类我的服务{ 私有MyRepository 存储库; @Autowired 公共无效setRepository(MyRepository存储库){ this.repository=存储库; } } ```` 3\.字段注入:直接在类的字段上使用\\`@Autowired\\` 注解。 ```` @服务 公共类我的服务{ @Autowired 私有MyRepository 存储库; } ```` # 风险 在Spring框架中,单例范围意味着整个应用程序中只会创建一个实例。对于上面提到的依赖注入中标注有@Component、@controller、@service、@Repository注解的类,默认都是单例的。这些类通常会包含一些静态成员(例如记录器),但所有非静态成员都应由Spring 容器管理,以便它们可以正确参与依赖注入。 如果这些单例类中有非静态成员,并且这些成员未传递@Autowired、aValue、@Inject 或 @Resource注解来注入,那么可能存在漏洞,因为这样的成员变量可能会被错误地用于状态管理。 在多用户并发环境中,单例bean 的状态管理(如果处理不当)可能会导致以下安全风险或问题: 1、线程安全问题:如果多个请求同时访问这个单例实例,同时修改其状态(非静态成员变量 2.数量),那么可能会出现线程安全问题。由于Spring 的单例bean 默认情况下不是线程安全的,因此这可能会导致数据不一致或竞争条件。 3.数据泄漏风险:正如您提到的,如果用户的会话数据被错误地存储在单例bean的成员变量中,其他用户可能会意外访问该数据。这会造成严重的隐私问题和数据泄露。 4. 不正确的业务执行:错误地假设每个用户都有自己的实例,业务执行可能会出错,从而导致不可预测的行为和潜在的错误。 在sonar的扫描规则中,有一个涉及到Spring依赖注入。 规则:java:S3749 级别:严重漏洞 规则名称:MembersofSpringcomponentsshouldbeinjected 解决方案:用\'@Autowired\'、\'@Resource\'、\'@Inject\'或\'@Value\'注释此成员,或将其删除 漏洞描述:Spring@Component、@Controller、@Service和@Repository类默认情况下都是单例,这意味着应用程序中只会实例化该类的一个实例。通常,这样的类可能有一些静态成员,例如记录器,但所有非静态成员都应该由Spring 管理。也就是说,它们应该具有以下注释之一:@Resource、@Inject、@Autowired 或@Value。 在这些类之一中拥有非注入成员可能表明尝试管理状态。因为它们是单例,所以这种尝试几乎肯定会最终将User1 会话中的数据暴露给User2。 当未使用@ConfigurationProperties 注释的单例@Component、@Controller、@Service 或@Repository 具有未使用以下其中一项注释的非静态成员时,可能会导致此问题。 以一段有风险的代码为例: ```` @控制器 公共类HelloWorld { 私有字符串名称=null; @RequestMapping(\'/greet\', 方法=GET) 公共字符串问候(字符串问候){ if (greetee !=null) { this.name=问候语; } return \'Hello \' + this.name; //如果greetee为null,你会看到前一个用户的数据 } } ```` 这段代码存在一个问题,当访问\\`/greet\\\` 路径时,在每次请求之间并没有清除\\`name\\\` 字段的值。这意味着,如果您首先访问\\`/greet?greetee=John\\`,然后访问\\`/greet?greetee=Doe\\`,则第二个请求将显示John 的数据,而不是Doe 的数据。 为了解决这个问题,你应该在每次请求之前初始化\\`name\\` 字段。您可以在非静态初始化方法上使用Java 的@PostConstruct 注释来完成此操作。这是修改后的代码: ```` @控制器 公共类HelloWorld { 私有字符串名称; @PostConstruct 私有无效初始化(){ this.name=null; } @RequestMapping(\'/greet\', 方法=GET) 公共字符串问候(@RequestParam(\'greetee\')字符串问候){ if (greetee !=null) { this.name=问候语; } return \'Hello \' + this.name; } } ```` 在此版本中,我们在类中定义了一个名为\\`init\\` 的私有方法,并使用了\\`@PostConstruct\\` 注解。 Spring 在实例化控制器时会自动调用此方法,确保每次请求时\\`name\\` 都被正确初始化为\\`null\\`。此外,我们还使用\\`@RequestParam\\\` 从URL 查询参数中显式获取\\`greetee\\\` 值。这可确保在每个请求之间正确清除\\`name\\` 字段。 #编码最佳实践 事实上,在实际的企业代码实践中,解决此类风险取决于良好的代码规范,并且重构旧代码是非常困难的。检测此规则的解决方法是将字段初始化为null,例如: ```` 私有环境env=null; 私有YYYAdaptor yyyAdaptor=null; 私有JAXBContext jaxbContext=null; ```` 当然,将字段声明为final也可以解决问题。 ```` 私人最终环境env; 私有最终YYYAdaptor yyyAdaptor; 私有最终JAXBContext jaxbContext; ```` 当然,即使有风险的代码是这样写的,也不一定会导致真正的安全问题。实际上有很多方法可以避免线程安全问题,例如: 1\. Double-Check Locking/双重检查锁定(Double-Check Locking) : 这是一种常见的线程安全实现方法。它使用同步块在类初始化期间仅执行一项实例化操作。这种方法在保证线程安全的同时提高了性能。 ```` 公共类单例{ 私有易失性静态单例实例; 私有单例(){} 公共静态单例getInstance() { 如果(实例==空){ 同步(Singleton.class){ 如果(实例==空){ 实例=新单例(); } } } 返回实例; } } ```` 2\.静态内部类:这种实现巧妙地利用了Java类加载机制。单例对象的创建在且仅在类加载时执行,因此它是线程安全的。 ```` 公共类单例{ 私有单例(){} 私有静态类SingletonHolder { 私有静态最终单例实例=new Singleton(); } 公共静态单例getInstance() { 返回SingletonHolder.INSTANCE; } } ```` 3\.枚举:使用枚举类型创建单例也是线程安全的,因为枚举类型的实例是在编译时确定的,不允许存在多个实例。 ```` 公共枚举单例{ 实例; 公共无效doSomething(){ //. } } //使用 Singleton.INSTANCE.doSomething(); ```` 因此,对于依赖注入可能带来的安全问题,仅仅检测变量是否被Spring托管的逻辑过于简单粗暴。对于开放解决方案的问题,安全从业者在制定白盒规则时应该采取更加谨慎的态度。转载自freebuf:[https://www.freebuf.com/articles/web/389199.html](https://www.freebuf.com/articles/web/389199.html) 作者:李曦
创建帐户或登录后发表意见