返回资讯中心
外部精选
软件工程
#开发者实时流

未转义的 Unicode 字符可将 Java 注释变为可执行代码

来源标题:A Unicode Escape Can Turn a Java Comment into Executable Code

如果 SDK 生成器在将 OpenAPI 文档复制到 Java 源代码时未对 Unicode 转义进行转义,注释中的 \u000a 会被编译器当作换行符处理,使其后的代码脱离注释并被执行。该行为已在 OpenJDK 21 上复现,且属于 Java 语言规范长期以来的规定。

Hacker News 最新khalidkhair2026/11/8 · 今天30 分钟阅读

以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。

当SDK生成器把OpenAPI规范中的文档说明复制到源代码里,却没有做转义处理时,会发生什么?

这个Java程序本该什么都不做。

它包含一个小类,类里有一个main()方法,还有一行看起来是注释掉的文本。我用javac编译并运行了它,结果却出乎意料。

class Example {
    public static void main(String[] args) {
        // Description: \u000a System.out.println("Hello");

看第三行。System.out.println()语句出现在//之后,按理说应该属于注释的一部分。

\u000a这个序列代表换行符。根据Java语言规范,Unicode转义在编译器识别注释、字符串和其他token之前就已经被处理了。

等到Java处理到注释时,源码实际上已经变成了这样:

class Example {
    public static void main(String[] args) {
        // Description:
        System.out.println("Hello");

编译器看到一个完全合法的程序,于是执行了这条语句。

我用OpenJDK 21复现了这个现象。这是Java语言规范的一部分,而且已经存在很长时间了。

真正有意思的是,那条原始注释可能是从哪儿来的。

开发者大概不会故意写这种东西。但代码生成器在从API规范复制描述时,却很可能产出非常相似的内容。

而生成器必须足够了解Java编译器,才能防止这种情况发生。

OpenAPI文档用结构化数据来描述API。

它包含端点路径、请求参数、响应schema、认证要求,以及客户端与服务器通信所需的其他信息。

paths:
  /users:
    get:
      summary: List users
      description: Returns the users available to the caller.

SDK生成器读取这份文档,生成对应的客户端方法。

/**
 * Returns the users available to the caller.
 */
public List<User> listUsers() {
    // ...
}

描述变成了生成SDK中的文档。开发者可以在IDE里把鼠标悬停在方法上,读到来自API规范的说明。

这是个有用的功能,实现起来似乎也很直接。

读取描述。放进注释里。写出源文件。

只不过,这段描述属于另一种语言。

在OpenAPI文档里,它是一个字符串。YAML解析器把它当作数据。它没理由关心这个字符串里是否包含Java注释定界符、Ruby插值语法或PHP结束标签。

一旦生成器把同样的字符串写进源文件,这些字符就获得了新的含义。

语言编译器不知道它们来自OpenAPI文档。它看到的只是构成程序一部分的字符。

这就是生成器必须守住的边界。

OpenAPI文档                           SDK生成器
description: YAML中的字符串    →    结构化数据变成源代码
                                      ↓
                                  上下文感知编码器
                                  针对Java注释进行转义
                                      ↓
直接文本插入                          描述保持为注释
字符串原样复制                        Java源码结构不变
                                      ↓
程序结构改变
一个Java序列离开了注释

描述从结构化数据跨越到源代码的边界

Java的例子展示了这条边界失效的一种方式。其他语言也有各自的方式,去解释那些在规范里看起来无害的文本。

PHP似乎是下一个自然的考察对象,因为它与注释和源文件边界之间有着不同寻常的关系。

PHP文件可以在PHP代码和PHP标签之外的内容之间切换。这对模板和服务器渲染页面很有用,但也引入了在生成源代码时不容忽视的行为。

<?php
// Description: Returns the current user.

通常的预期是,//之后的所有内容一直到换行符为止都保持注释状态。

但结束标签?>会终止当前的PHP代码块,即使它出现在单行注释里也是如此。

PHP手册明确记录了这一行为。

我用PHP 8.5复现了它。行注释里的结束标签让解释器退出了PHP模式。当后续出现开始标签时,PHP又恢复了代码执行。

对于手写PHP的人来说,这个行为可以理解。PHP的开始和结束标签是其语法的基本组成部分。

但对SDK生成器来说,这又创造了一个周边模板未必能按预期容纳输入的地方。

假设生成器用PHP行注释来记录一个方法。描述来自OpenAPI文件,模板作者以为注释前缀会让其中的内容变得无害。

这个假设只有在生成器考虑了PHP标签处理的前提下才成立。

Java的Unicode处理和PHP的标签转换是两种不同的机制,但它们给生成的源代码造成了同样的问题:作为文档提供的字符,能够影响语言对文件的解析方式。

补救措施必须在源代码到达编译器或解释器之前就实施。

在PHP中,这意味着在写注释时处理标签定界符。在Java中,这意味着在注释被识别之前就考虑Unicode转义的处理。

而注释只是问题的一部分。

文档字段并不是OpenAPI中唯一的自由文本值。

规范中还包含示例、默认值、枚举值以及其他字符串,生成器经常把它们复制到源代码字面量中。

Ruby提供了一个很好的例子,说明为什么这些值同样需要小心处理。

假设规范中包含这样一个示例:

example: 'Result: #{1 + 1}'

预期的值是字面字符串Result: #{1 + 1}。

现在想象一个Ruby生成器构造出下面这个常量:

EXAMPLE = "Result: #{1 + 1}"

(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →