Swagger-Parser race condition leads to Cross-Thread Data Contamination
好的,我现在需要帮用户总结一篇关于swagger-parser库线程安全问题的文章。用户要求控制在100字以内,不需要特定的开头,直接描述内容。 首先,我仔细阅读了文章内容。文章主要讨论了swagger-parser库在处理OpenAPI 3.1规范时的线程安全性问题。当多个线程同时解析时,结果可能会被交换,导致敏感信息泄露。 接下来,我注意到文章提到了漏洞的严重性、影响范围以及测试方法。漏洞被定为高危,因为攻击者可以利用并发解析请求来窃取内部端点信息。测试使用了特定的开源API规范,并且在OpenAPI 3.1文件中能够稳定复现问题,但在3.0中没有出现。 然后,我考虑如何将这些关键点浓缩到100字以内。需要包括库名称、问题类型(线程不安全)、受影响的规范版本(3.1)、并发解析导致的结果交换以及潜在的安全风险。 最后,我组织语言,确保信息准确且简洁。避免使用复杂的术语,让用户一目了然地理解问题所在和影响。 </think> swagger-parser库在处理OpenAPI 3.1规范时存在线程不安全问题,可能导致并发解析结果交换。该漏洞允许攻击者通过触发并发解析请求来窃取敏感API信息。测试表明该问题仅影响OpenAPI 3.1文件。 2026-3-10 00:0:59 Author: github.com(查看原文) 阅读量:4 收藏

Summary

The swagger-parser library is not thread safe for OpenAPI 3.1 specifications. When parsing on multiple threads concurrently it is possible for the parsing results for specs on concurrent threads to be swapped.

Severity

High - This vulnerability allows an attacker to intercept or manipulate sensitive API specifications by triggering concurrent parsing requests, leading to the unauthorized disclosure of internal endpoints.

Proof of Concept

The specs used for testing were retrieved from this open source repository https://github.com/APIs-guru/openapi-directory/blob/main/APIs. To get equivalent 3.0 and 3.1 files for testing we changed the header on the 3.0 files used to "openapi: 3.1.2". Two example specs that were used to replicate the issue were fec.gov_1.0_openapi.yaml and figshare.com_2.0.0_openapi.yaml.

We verified this issue by running a test which parses multiple 3.1 files at the same time on different threads. The specs used have a different number of operations that contain operation Ids.

We found an expected number of operations without an operation Id for the specs we were testing with. For (fec.gov_1.0_openapi.yaml this is 92 for figshare.com_2.0.0_openapi.yaml it is 1).

Created a maven project with the following configuration.

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>io.swagger.parser.test</groupId>
    <artifactId>thread-issue-repro</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>io.swagger.parser.v3</groupId>
            <artifactId>swagger-parser</artifactId>
            <version>2.1.35</version>
        </dependency>
        <dependency>
            <groupId>io.swagger.core.v3</groupId>
            <artifactId>swagger-models</artifactId>
            <version>2.2.39</version>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>33.2.1-jre</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.32</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.11</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.15.2</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-yaml</artifactId>
            <version>2.15.2</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <mainClass>io.swagger.parser.test.ParserTest</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  1. We use this project to run the following test file
package io.swagger.parser.test;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.parser.OpenAPIV3Parser;
import io.swagger.v3.parser.core.models.ParseOptions;
import io.swagger.v3.parser.core.models.SwaggerParseResult;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ParserTest {
  private static final Logger logger = LoggerFactory.getLogger(ParserTest.class);
  private static final int NUM_THREADS = 10;
  private static final int NUM_RUNS = 50; // Total parsing attempts

  private static final List<String> FILE_PATHS =
      ImmutableList.of(
          "{test-file1-path}/{test-file1-name}",
          "{test-file2-path}/{test-file2-name}");

  private static final Map<String, Long> EXPECTED_MISSING_OP_IDS =
      ImmutableMap.of(
          "ftest-file1-name}", 92L /* Expected operations missing op ids */,
          "{test-file2-name}", 1L /* Expected operations missing op ids */);

  public static void main(String[] args) throws InterruptedException {
    logger.info("Starting test with files: {}", FILE_PATHS);

    ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS);
    ParseOptions options = new ParseOptions();
    options.setResolve(true);
    options.setResolveFully(false);

    AtomicInteger runCount = new AtomicInteger(0);
    AtomicBoolean inconsistencyFound = new AtomicBoolean(false);

    for (int i = 0; i < NUM_RUNS; i++) {
      final int fileIndex = i % FILE_PATHS.size();
      final String filePathStr = FILE_PATHS.get(fileIndex);
      Path filePath = Paths.get(filePathStr);
      String shortFileName = filePath.getFileName().toString();

      executor.submit(
          () -> {
            int runId = runCount.incrementAndGet();
            try {
              long startTime = System.currentTimeMillis();
              // Verify we're creating one of these per request
              SwaggerParseResult result =
                  new OpenAPIV3Parser().readLocation(filePath.toString(), null, options);
              long endTime = System.currentTimeMillis();

              OpenAPI openAPI = result.getOpenAPI();
              if (openAPI != null) {
                int pathCount = openAPI.getPaths() != null ? openAPI.getPaths().size() : 0;
                long operationsMissingId = countOperationsMissingId(openAPI);
                long expectedMissing = EXPECTED_MISSING_OP_IDS.getOrDefault(shortFileName, -1L);

                if (operationsMissingId != expectedMissing) {
                  inconsistencyFound.set(true);
                  logger.error(
                      "INCONSISTENCY DETECTED - Run {}: Thread {}: File: {}, Expected Missing"
                          + " OpIds: {}, Got: {}",
                      runId,
                      Thread.currentThread().getId(),
                      shortFileName,
                      expectedMissing,
                      operationsMissingId);
                }

                logger.info(
                    "Run {}: Thread {}: File: {}, Parse time: {}ms, Paths: {}, Missing OpIds: {},"
                        + " Messages: {}",
                    runId,
                    Thread.currentThread().getId(),
                    shortFileName,
                    (endTime - startTime),
                    pathCount,
                    operationsMissingId,
                    result.getMessages());
              } else {
                logger.warn(
                    "Run {}: Thread {}: File: {}, Parse time: {}ms, OpenAPI object is NULL,"
                        + " Messages: {}",
                    runId,
                    Thread.currentThread().getId(),
                    shortFileName,
                    (endTime - startTime),
                    result.getMessages());
              }
            } catch (Exception e) {
              logger.error(
                  "Run {}: Thread {}: File: {}, Exception: {}",
                  runId,
                  Thread.currentThread().getId(),
                  shortFileName,
                  e.getMessage(),
                  e);
            }
          });
    }

    executor.shutdown();
    executor.awaitTermination(3, TimeUnit.MINUTES);
    logger.info("Test complete.");

    if (inconsistencyFound.get()) {
      logger.error("FAIL: INCONSISTENCIES WERE OBSERVED!");
    } else {
      logger.info("SUCCESS: No inconsistencies observed.");
    }
  }

  private static long countOperationsMissingId(OpenAPI openAPI) {
    if (openAPI.getPaths() == null) {
      return 0;
    }
    long missing = 0;
    for (Map.Entry<String, PathItem> pathEntry : openAPI.getPaths().entrySet()) {
      PathItem pathItem = pathEntry.getValue();
      if (pathItem == null) continue;
      for (Operation operation : pathItem.readOperations()) {
        if (operation != null
            && (operation.getOperationId() == null || operation.getOperationId().isEmpty())) {
          missing++;
        }
      }
    }
    return missing;
  }
}

Further Analysis

Expected Behavior
All threads running should have the same number of operations without ids as the baseline test.

Actual Behavior
OpenAPI 3.1 files that are parsed in parallel often do not have the same number of operations without ids as those that are parsed by themselves. We were able to consistently reproduce these results for OpenAPI 3.1 files in our testing, however, we were not able to reproduce the results in OpenAPI 3.0. Therefore, we believe that this only affects OpenAPI 3.1 parsing.

Timeline

Date reported: 10/27/02025
Date fixed:
Date disclosed: 03/10/2026


文章来源: https://github.com/google/security-research/security/advisories/GHSA-2237-hv52-mmg9
如有侵权请联系:admin#unsafe.sh