数据和代码要分清(java中如何避免安全问题)

简介: Web 安全方面的很多漏洞,都是源自把数据当成了代码来执行,也就是注入类问题

Web 安全方面的很多漏洞,都是源自把数据当成了代码来执行,也就是注入类问题,比如:


客户端提供给服务端的查询值,是一个数据,会成为 SQL 查询的一部分。攻击者通过修改这个值注入一些 SQL,来达到在服务端运行 SQL 的目的,相当于把查询条件的数据变为了查询代码。这个叫做SQL 注入。


对于规则引擎,我们可能会用动态语言做一些计算,和 SQL 注入一样外部传入的数据只能当做数据使用,如果被攻击者利用传入了代码,那么代码可能就会被动态执行。这个叫代码注入。


对于用户注册、留言评论这些功能,服务端会从客户端收集一些信息,本来用户名、邮箱这类信息是纯文本信息,但是攻击者把信息替换为了 JavaScript 代码。这些信息在页面呈现时,可能就相当于执行了 JavaScript 代码。而服务端可能把这样的代码,当作普通信息保存到了数据库。攻击者通过构建 JavaScript 代码来实现修改页面呈现、盗取信息,甚至蠕虫攻击的方式,叫做 XSS(跨站脚本)攻击。

一、SQL注入能做的比你想象的还要多

如果你还不知道什么是SQL注入,可以查看我的这篇文章进行了解天天听SQL注入,SQL注入到底是怎么注入的?


SQL注入最经典的例子,就是传入 or 1=1 作为密码实现登录。而这种简单的攻击方式,在十几年前可以突破很多后台系统,但现在不行了。


近几年大家的安全意识增强了,都知道使用参数化查询来避免SQL注入问题(例如mybatis中的#{}取值方式)。但还是有几点需要注意


第一、SQL注入不只是存在于get请求(不只是存在URL传参中)


从注入的复杂程度来说,修改URL和修改post请求的请求体没有区别,因为攻击者是使用工具执行请求,而不是修改浏览器上面的URL执行请求的。甚至cookie也有可能用来注入,任何提供数据的地方都可能称为注入点。


第二、不返回数据的接口同样存在注入问题


攻击者完全可以构造不正确的SQL语句,使执行出错,如果服务端直接显示了错误信息,那攻击者需要的数据很有可能被带出来。即使没有错误信息,攻击者也可以通过盲注的方式进行攻击。


对于错误信息,在开发中服务端应当使用全局错误捕获来拦截所有的异常,并封装好自定义的错误提示返回客户端。


第三、SQL注入不仅仅是可以用来突破登录


SQL注入完全可以实现下载整个数据库的内容。其次根据木桶原理,整个系统的安全性受限于安全级别最低的那块短板。因此对于安全问题,并不是只加强防范某些重点模块就行。


在mybatis中,{}”是参数化的方式,“${}”只是占位符替换。


比如 LIKE 语句。因为使用“#{}”会为参数带上单引号,导致 LIKE 语法错误,所以一些开发人员会退而求其次,选择“${}”的方式,比如


@Select("SELECT id,name FROM `userdata` WHERE name LIKE '%${name}%'")

List<UserData> selectByName(@Param("name") String name);

正确的做法是,使用“#{}”来参数化 name 参数,对于 LIKE 操作可以使用 CONCAT 函数来拼接 % 符号:


@Select("SELECT id,name FROM `userdata` WHERE name LIKE CONCAT('%',#{name},'%')")

List<UserData> selectByNameRight(@Param("name") String name);

或者使用bind标签对原字符串前后绑定百分号后再使用#{}获取绑定后的值使用


二、XSS必须严防丝堵

XSS 问题的根源是,原本是让用户传入或输入正常数据的地方,被攻击者替换为了 JavaScript 脚本,页面没有经过转义直接显示了这个数据,然后脚本就被执行了。更严重的是,脚本没有经过转义就保存到了数据库中,随后页面加载数据的时候,数据中混入的脚本又当做代码执行了。攻击者就可以利用这个漏洞来盗取敏感数据,诱骗用户访问钓鱼网站等。


写一段代码测试下。首先,服务端定义两个接口,其中 index 接口查询用户名信息返回给xss页面,save 接口使用 @RequestParam 注解接收用户名,并创建用户保存到数据库;然后,重定向浏览器到 index 接口


@Controller
@RequestMapping("xss")
public class XssController {
    @Resource
    private UserRepository userRepository;
    @GetMapping
    public String index(ModelMap modelMap) {
        //查数据库
        User user = userRepository.findById(1L).orElse(new User());
        //给View提供Model
        modelMap.addAttribute("username", user.getName());
        return "xss";
    }
    @PostMapping
    public String save(@RequestParam("username") String username, HttpServletRequest request) {
        User user = new User();
        user.setId(1L);
        user.setName(username);
        userRepository.save(user);
        //保存完成后重定向到首页
        return "redirect:/xss/";
    }
 }
//用户类,同时作为DTO和Entity
@Entity
@Data
public class User {
    @Id
    private Long id;
    private String name;
}

使用 Thymeleaf 模板引擎渲染页面。代码比较简单,页面加载的时候会在标签显示用户名,用户输入用户名提交后调用 save 接口创建用户


<div style="font-size: 14px">
    <form id="myForm" method="post" th:action="@{/xss/}">
        <label th:utext="${username}"/>
        <input id="username" name="username" size="100" type="text"/>
        <button th:text="Register" type="submit"/>
    </form>
</div>

打开xss页面后,在文本框中输入 <script>alert('test')</script> 点击 Register 按钮提交,页面会弹出 alert 对话框,并且脚本呗保存到了数据库



大家可能想到了,解决方式就是 HTML 转码。既然是通过 @RequestParam 来获取请求参数,那就定义一个 @InitBinder 实现数据绑定的时候,对字符串进行转码即可:


@ControllerAdvice
public class SecurityAdvice {
    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        //注册自定义的绑定器
        binder.registerCustomEditor(String.class, new PropertyEditorSupport() {
            @Override
            public String getAsText() {
                Object value = getValue();
                return value != null ? value.toString() : "";
            }
            @Override
            public void setAsText(String text) {
                //赋值时进行HTML转义
                setValue(text == null ? null : HtmlUtils.htmlEscape(text));
            }
        });
    }
}


针对这个场景,此做法确实是可行的,脚本没有被执行,也被转码后保存到了数据库中



但是这种方式并没有从根源解决问题,@InitBinder 是 Spring Web 层面的处理逻辑,如果有代码不通过 @RequestParam 来获取数据,而是直接从 HTTP 请求获取数据的话,这种方式就不会奏效。比如使用request.getParameter()。


更合理的解决方法是顶一个filter,实现servlet层面的统一参数替换。


@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class XssFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        chain.doFilter(new XssRequestWrapper((HttpServletRequest) request), response);
    }
}
public class XssRequestWrapper extends HttpServletRequestWrapper {
    public XssRequestWrapper(HttpServletRequest request) {
        super(request);
    }
    @Override
    public String[] getParameterValues(String parameter) {
        //获取多个参数值的时候对所有参数值应用clean方法逐一清洁
        return Arrays.stream(super.getParameterValues(parameter)).map(this::clean).toArray(String[]::new);
    }
    @Override
    public String getHeader(String name) {
        //同样清洁请求头
        return clean(super.getHeader(name));
    }
    @Override
    public String getParameter(String parameter) {
        //获取参数单一值也要处理
        return clean(super.getParameter(parameter));
    }
    //clean方法就是对值进行HTML转义
    private String clean(String value) {
      return StringUtils.isEmpty(value)? "" : HtmlUtils.htmlEscape(value);
    }
}


不过,这种方式还是不够彻底,原因是无法处理通过 @RequestBody 注解提交的 JSON 数据。处理JSON数据,需要自定义一个 Jackson 反列化器,来实现反序列化时的字符串的 HTML 转义


@Bean
public Module xssModule() {
    SimpleModule module = new SimpleModule();
    module.module.addDeserializer(String.class, new XssJsonDeserializer());
    return module;
}
public class XssJsonDeserializer extends JsonDeserializer<String> {
    @Override
    public String deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        String value = jsonParser.getValueAsString();
        if (value != null) {
            //对于值进行HTML转义
            return HtmlUtils.htmlEscape(value);
        }
        return value;
    }
    @Override
    public Class<String> handledType() {
        return String.class;
    }
}


这样就实现了既能转义 Get/Post 通过请求参数提交的数据,又能转义请求体中直接提交的 JSON 数据。


你可能觉得做到这里,我们的防范已经很全面了,但其实不是。这种只能堵新漏,确保新数据进入数据库之前转义。如果因为之前的漏洞,数据库中已经保存了一些 JavaScript 代码,那么读取的时候同样可能出问题。因此,我们还要实现数据读取的时候也转义。


之前我们处理了 JSON 反序列化问题,那么就需要同样处理序列化,实现数据从数据库中读取的时候转义,否则读出来的 JSON 可能包含 JavaScript 代码。


修改之前的 SimpleModule 加入自定义序列化器,并且实现序列化时处理字符串转义


@Bean
public Module xssModule() {
    SimpleModule module = new SimpleModule();
    module.addDeserializer(String.class, new XssJsonDeserializer());
    module.addSerializer(String.class, new XssJsonSerializer());
    return module;
}
public class XssJsonSerializer extends JsonSerializer<String> {
    @Override
    public Class<String> handledType() {
        return String.class;
    }
    @Override
    public void serialize(String value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        if (value != null) {
            //对字符串进行HTML转义
            jsonGenerator.writeString(HtmlUtils.htmlEscape(value));
        }
    }
}


经过修改后,即使数据库中已经保存了 JavaScript 代码,呈现的时候也只能作为 HTML 显示了。现在,对于进和出两个方向,都实现了补漏。


相关文章
|
20小时前
|
Java 编译器
滚雪球学Java(36):玩转Java方法重载和可变参数,让你的代码更灵活
【5月更文挑战第11天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
5 0
滚雪球学Java(36):玩转Java方法重载和可变参数,让你的代码更灵活
|
1天前
|
前端开发 Java
基于Java爬取微博数据(二) 正文长文本+导出数据Excel
【5月更文挑战第12天】基于Java爬取微博数据,正文长文本+导出数据Excel
|
1天前
|
存储 安全 Java
Java一分钟之-泛型擦除与类型安全
【5月更文挑战第20天】Java泛型采用类型擦除机制,在编译期间移除泛型信息,但在编译阶段提供类型安全检查。尽管需要类型转换且可能产生警告,但可以通过特定语法避免。使用泛型时应注意自动装箱拆箱影响性能,无界通配符仅允许读取。理解这些特性有助于编写更安全的代码。
27 4
|
2天前
|
Java 测试技术
如何提高Java代码的可读性
Java是一种常用的编程语言,但是写出易懂且可读性高的代码却是一项挑战。本文将分享一些技巧和建议,帮助您提高Java代码的可读性和可维护性。
|
2天前
|
网络安全 流计算 Python
实时计算 Flink版操作报错合集之Flink sql-client 针对kafka的protobuf格式数据建表,报错:java.lang.ClassNotFoundException 如何解决
在使用实时计算Flink版过程中,可能会遇到各种错误,了解这些错误的原因及解决方法对于高效排错至关重要。针对具体问题,查看Flink的日志是关键,它们通常会提供更详细的错误信息和堆栈跟踪,有助于定位问题。此外,Flink社区文档和官方论坛也是寻求帮助的好去处。以下是一些常见的操作报错及其可能的原因与解决策略。
16 1
|
4天前
|
数据采集 前端开发 Java
基于Java爬取微博数据(一) 微博主页正文列表数据
【5月更文挑战第9天】讲述如何通过 Java 爬取微博数据 微博主页正文列表数据,以及相应的注意点
|
4天前
|
消息中间件 关系型数据库 网络安全
实时计算 Flink版操作报错合集之Flink sql-client 针对kafka的protobuf格式数据建表,报错:java.lang.ClassNotFoundException 如何解决
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
14 1
|
4天前
|
安全 Java API
JAVA-不安全的反射--RCE
JAVA不安全的反射造成的RCE小案例
|
5天前
|
缓存 安全 Java
7张图带你轻松理解Java 线程安全,java缓存机制面试
7张图带你轻松理解Java 线程安全,java缓存机制面试
|
6天前
|
Java Kotlin
java调用kotlin代码编译报错“找不到符号”的问题
java调用kotlin代码编译报错“找不到符号”的问题
17 10