**security-research** Public

# Swagger-Parser race condition leads to Cross-Thread Data Contamination

## Package

## Affected versions

## Patched versions

## Description

### 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.

“`
4.0.0 io.swagger.parser.test thread-issue-repro 1.0-SNAPSHOT jar 11 11 UTF-8 io.swagger.parser.v3 swagger-parser 2.1.35 io.swagger.core.v3 swagger-models 2.2.39 com.google.guava guava 33.2.1-jre org.slf4j slf4j-api 1.7.32 ch.qos.logback logback-classic 1.2.11 com.fasterxml.jackson.core jackson-databind 2.15.2 com.fasterxml.jackson.dataformat jackson-dataformat-yaml 2.15.2 org.apache.maven.plugins maven-compiler-plugin 3.8.1 11 11 org.codehaus.mojo exec-maven-plugin 3.1.0 io.swagger.parser.test.ParserTest
“`

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 FILE_PATHS = ImmutableList.of( “{test-file1-path}/{test-file1-name}”, “{test-file2-path}/{test-file2-name}”); private static final Map 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 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