CVE-2025-53103
Description
JUnit is a testing framework for Java and the JVM. From version 5.12.0 to 5.13.1, JUnit's support for writing Open Test Reporting XML files can leak Git credentials. The impact depends on the level of the access token exposed through the OpenTestReportGeneratingListener. If these test reports are published or stored anywhere public, then there is the possibility that a rouge attacker can steal the token and perform elevated actions by impersonating the user or app. This issue as been patched in version 5.13.2.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.junit.platform:junit-platform-reportingMaven | >= 5.12.0, < 5.13.2 | 5.13.2 |
Affected products
1Patches
2e51deb24d2f4d4fc834c8c1cMerge commit from fork
7 files changed · +313 −112
documentation/documentation.gradle.kts+1 −0 modified@@ -161,6 +161,7 @@ tasks { args.addAll("execute") args.addAll("--scan-classpath") args.addAll("--config=junit.platform.reporting.open.xml.enabled=true") + args.addAll("--config=junit.platform.reporting.open.xml.git.enabled=true") args.addAll("--config=junit.platform.output.capture.stdout=true") args.addAll("--config=junit.platform.output.capture.stderr=true") args.addAll("--config=junit.platform.discovery.issue.severity.critical=info")
documentation/src/docs/asciidoc/release-notes/release-notes-5.13.2.adoc+5 −1 modified@@ -21,7 +21,11 @@ repository on GitHub. [[release-notes-5.13.2-junit-platform-deprecations-and-breaking-changes]] ==== Deprecations and Breaking Changes -* ❓ +* Including information about the Git repository (such as the commit hash and branch name) + in the Open Test Reporting XML format is now an opt-in feature that can be enabled via a + configuration parameter. Please refer to the + <<../user-guide/index.adoc#junit-platform-reporting-open-test-reporting, User Guide>> + for details. [[release-notes-5.13.2-junit-platform-new-features-and-improvements]] ==== New Features and Improvements
documentation/src/docs/asciidoc/user-guide/advanced-topics/junit-platform-reporting.adoc+4 −2 modified@@ -36,10 +36,12 @@ event-based format specified by {OpenTestReporting} which supports all features JUnit Platform such as hierarchical test structures, display names, tags, etc. The listener is auto-registered and can be configured via the following -<<running-tests-config-params, configuration parameter>>: +<<running-tests-config-params, configuration parameters>>: `junit.platform.reporting.open.xml.enabled=true|false`:: - Enable/disable writing the report. + Enable/disable writing the report; defaults to `false`. +`junit.platform.reporting.open.xml.git.enabled=true|false`:: + Enable/disable including information about the Git repository (see https://github.com/ota4j-team/open-test-reporting#git[Git extension schema] of open-test-reporting); defaults to `false`. If enabled, the listener creates an XML report file named `open-test-report.xml` in the configured <<junit-platform-reporting-output-directory, output directory>>.
gradle/plugins/common/src/main/kotlin/junitbuild.testing-conventions.gradle.kts+1 −0 modified@@ -130,6 +130,7 @@ tasks.withType<Test>().configureEach { jvmArgumentProviders += CommandLineArgumentProvider { listOf( "-Djunit.platform.reporting.open.xml.enabled=true", + "-Djunit.platform.reporting.open.xml.git.enabled=true", "-Djunit.platform.reporting.output.dir=${reports.junitXml.outputLocation.get().asFile.absolutePath}/junit-{uniqueNumber}", ) }
junit-platform-reporting/src/main/java/org/junit/platform/reporting/open/xml/GitInfoCollector.java+209 −0 added@@ -0,0 +1,209 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.reporting.open.xml; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UncheckedIOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; + +import org.junit.platform.commons.util.ExceptionUtils; +import org.junit.platform.commons.util.StringUtils; + +/** + * @since 5.13.2 + */ +interface GitInfoCollector { + + static Optional<GitInfoCollector> get(Path workingDir) { + ProcessExecutor executor = new ProcessExecutor(workingDir); + boolean gitInstalled = executor.exec("git", "--version").isPresent(); + return gitInstalled ? Optional.of(new CliGitInfoCollector(executor)) : Optional.empty(); + } + + Optional<String> getOriginUrl(); + + Optional<String> getBranch(); + + Optional<String> getCommitHash(); + + Optional<String> getStatus(); + + class CliGitInfoCollector implements GitInfoCollector { + + private static final String ALLOWED_USERNAME = "git"; + private static final String USER_INFO_REPLACEMENT = "***"; + private static final String USER_INFO_SEPARATOR = "@"; + + private final ProcessExecutor executor; + + public CliGitInfoCollector(ProcessExecutor executor) { + this.executor = executor; + } + + @Override + public Optional<String> getOriginUrl() { + return executor.exec("git", "config", "--get", "remote.origin.url") // + .filter(StringUtils::isNotBlank) // + .flatMap(this::toGitUrl) // + .flatMap(this::obfuscateUserInfo); + } + + @Override + public Optional<String> getBranch() { + return executor.exec("git", "rev-parse", "--abbrev-ref", "HEAD") // + .filter(StringUtils::isNotBlank); + } + + @Override + public Optional<String> getCommitHash() { + return executor.exec("git", "rev-parse", "--verify", "HEAD") // + .filter(StringUtils::isNotBlank); + } + + @Override + public Optional<String> getStatus() { + return executor.exec("git", "status", "--porcelain"); + } + + private Optional<String> obfuscateUserInfo(GitUrl gitUrl) { + try { + if (gitUrl.uri.getUserInfo() != null) { + URI newUri = new URI(gitUrl.uri.getScheme(), USER_INFO_REPLACEMENT, gitUrl.uri.getHost(), + gitUrl.uri.getPort(), gitUrl.uri.getPath(), gitUrl.uri.getQuery(), gitUrl.uri.getFragment()); + return Optional.of(newUri.toString().substring(gitUrl.addedPrefix.length())); + } + if (gitUrl.uri.getAuthority() != null && gitUrl.uri.getAuthority().contains(USER_INFO_SEPARATOR)) { + String oldAuthority = gitUrl.uri.getAuthority(); + String[] parts = oldAuthority.split(USER_INFO_SEPARATOR, 2); + if (!ALLOWED_USERNAME.equals(parts[0])) { + String newAuthority = USER_INFO_REPLACEMENT + USER_INFO_SEPARATOR + parts[1]; + URI newUri = new URI(gitUrl.uri.getScheme(), newAuthority, gitUrl.uri.getPath(), + gitUrl.uri.getQuery(), gitUrl.uri.getFragment()); + return Optional.of(newUri.toString().substring(gitUrl.addedPrefix.length())); + } + } + return Optional.of(gitUrl.rawValue); + } + catch (URISyntaxException e) { + return Optional.empty(); + } + } + + private Optional<GitUrl> toGitUrl(String remoteUrl) { + try { + return Optional.of(new GitUrl(remoteUrl, new URI(remoteUrl), "")); + } + catch (URISyntaxException ex) { + try { + return Optional.of(new GitUrl(remoteUrl, new URI("ssh://" + remoteUrl), "ssh://")); + } + catch (URISyntaxException ignore) { + return Optional.empty(); + } + } + } + } + + class ProcessExecutor { + + private final Path workingDir; + + ProcessExecutor(Path workingDir) { + this.workingDir = workingDir; + } + + Optional<String> exec(String... args) { + + Process process = startProcess(args); + + try (Reader out = newBufferedReader(process.getInputStream()); + Reader err = newBufferedReader(process.getErrorStream())) { + + StringBuilder output = new StringBuilder(); + readAllChars(out, (chars, numChars) -> output.append(chars, 0, numChars)); + + readAllChars(err, (__, ___) -> { + // ignore + }); + + boolean terminated = process.waitFor(10, TimeUnit.SECONDS); + return terminated && process.exitValue() == 0 ? Optional.of(trimAtEnd(output)) : Optional.empty(); + } + catch (InterruptedException e) { + throw ExceptionUtils.throwAsUncheckedException(e); + } + catch (IOException ignore) { + return Optional.empty(); + } + finally { + process.destroyForcibly(); + } + } + + private static BufferedReader newBufferedReader(InputStream stream) { + return new BufferedReader(new InputStreamReader(stream, Charset.defaultCharset())); + } + + private Process startProcess(String[] command) { + Process process; + try { + process = new ProcessBuilder().directory(workingDir.toFile()).command(command).start(); + } + catch (IOException e) { + throw new UncheckedIOException("Failed to start process", e); + } + return process; + } + + private static void readAllChars(Reader reader, BiConsumer<char[], Integer> consumer) throws IOException { + char[] buffer = new char[1024]; + int numChars; + while ((numChars = reader.read(buffer)) != -1) { + consumer.accept(buffer, numChars); + } + } + + private static String trimAtEnd(StringBuilder value) { + int endIndex = value.length(); + for (int i = value.length() - 1; i >= 0; i--) { + if (Character.isWhitespace(value.charAt(i))) { + endIndex--; + break; + } + } + return value.substring(0, endIndex); + } + } + + class GitUrl { + + private final String rawValue; + private final URI uri; + private final String addedPrefix; + + GitUrl(String rawValue, URI uri, String addedPrefix) { + this.rawValue = rawValue; + this.uri = uri; + this.addedPrefix = addedPrefix; + } + } + +}
junit-platform-reporting/src/main/java/org/junit/platform/reporting/open/xml/OpenTestReportGeneratingListener.java+22 −93 modified@@ -51,30 +51,20 @@ import static org.opentest4j.reporting.events.root.RootFactory.reported; import static org.opentest4j.reporting.events.root.RootFactory.started; -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; import java.io.UncheckedIOException; import java.net.InetAddress; import java.net.UnknownHostException; -import java.nio.charset.Charset; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; import java.time.LocalDateTime; import java.util.Map; -import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.BiConsumer; import org.apiguardian.api.API; import org.junit.platform.commons.JUnitException; -import org.junit.platform.commons.util.ExceptionUtils; -import org.junit.platform.commons.util.StringUtils; import org.junit.platform.engine.ConfigurationParameters; import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.TestSource; @@ -110,6 +100,7 @@ public class OpenTestReportGeneratingListener implements TestExecutionListener { static final String ENABLED_PROPERTY_NAME = "junit.platform.reporting.open.xml.enabled"; + static final String GIT_ENABLED_PROPERTY_NAME = "junit.platform.reporting.open.xml.git.enabled"; private final AtomicInteger idCounter = new AtomicInteger(); private final Map<UniqueId, String> inProgressIds = new ConcurrentHashMap<>(); @@ -140,19 +131,23 @@ public void testPlanExecutionStarted(TestPlan testPlan) { Path eventsXml = outputDir.resolve("open-test-report.xml"); try { eventsFileWriter = Events.createDocumentWriter(namespaceRegistry, eventsXml); - reportInfrastructure(); + reportInfrastructure(config); } catch (Exception e) { throw new JUnitException("Failed to initialize XML events file: " + eventsXml, e); } } } - private Boolean isEnabled(ConfigurationParameters config) { + private boolean isEnabled(ConfigurationParameters config) { return config.getBoolean(ENABLED_PROPERTY_NAME).orElse(false); } - private void reportInfrastructure() { + private boolean isGitEnabled(ConfigurationParameters config) { + return config.getBoolean(GIT_ENABLED_PROPERTY_NAME).orElse(false); + } + + private void reportInfrastructure(ConfigurationParameters config) { eventsFileWriter.append(infrastructure(), infrastructure -> { try { String hostName = InetAddress.getLocalHost().getHostName(); @@ -168,89 +163,23 @@ private void reportInfrastructure() { .append(fileEncoding(System.getProperty("file.encoding"))) // .append(heapSize(), heapSize -> heapSize.withMax(Runtime.getRuntime().maxMemory())); - addGitInfo(infrastructure); + if (isGitEnabled(config)) { + GitInfoCollector.get(workingDir).ifPresent(git -> addGitInfo(infrastructure, git)); + } }); } - private void addGitInfo(Infrastructure infrastructure) { - boolean gitInstalled = exec("git", "--version").isPresent(); - if (gitInstalled) { - exec("git", "config", "--get", "remote.origin.url") // - .filter(StringUtils::isNotBlank) // - .ifPresent( - gitUrl -> infrastructure.append(repository(), repository -> repository.withOriginUrl(gitUrl))); - exec("git", "rev-parse", "--abbrev-ref", "HEAD") // - .filter(StringUtils::isNotBlank) // - .ifPresent(branch -> infrastructure.append(branch(branch))); - exec("git", "rev-parse", "--verify", "HEAD") // - .filter(StringUtils::isNotBlank) // - .ifPresent(gitCommitHash -> infrastructure.append(commit(gitCommitHash))); - exec("git", "status", "--porcelain") // - .ifPresent(statusOutput -> infrastructure.append(status(statusOutput), - status -> status.withClean(statusOutput.isEmpty()))); - } - } - - Optional<String> exec(String... args) { - - Process process = startProcess(args); - - try (Reader out = newBufferedReader(process.getInputStream()); - Reader err = newBufferedReader(process.getErrorStream())) { - - StringBuilder output = new StringBuilder(); - readAllChars(out, (chars, numChars) -> output.append(chars, 0, numChars)); - - readAllChars(err, (__, ___) -> { - // ignore - }); - - boolean terminated = process.waitFor(10, TimeUnit.SECONDS); - return terminated && process.exitValue() == 0 ? Optional.of(trimAtEnd(output)) : Optional.empty(); - } - catch (InterruptedException e) { - throw ExceptionUtils.throwAsUncheckedException(e); - } - catch (IOException ignore) { - return Optional.empty(); - } - finally { - process.destroyForcibly(); - } - } - - private static BufferedReader newBufferedReader(InputStream stream) { - return new BufferedReader(new InputStreamReader(stream, Charset.defaultCharset())); - } - - private Process startProcess(String[] command) { - Process process; - try { - process = new ProcessBuilder().directory(workingDir.toFile()).command(command).start(); - } - catch (IOException e) { - throw new UncheckedIOException("Failed to start process", e); - } - return process; - } - - private static void readAllChars(Reader reader, BiConsumer<char[], Integer> consumer) throws IOException { - char[] buffer = new char[1024]; - int numChars; - while ((numChars = reader.read(buffer)) != -1) { - consumer.accept(buffer, numChars); - } - } - - private static String trimAtEnd(StringBuilder value) { - int endIndex = value.length(); - for (int i = value.length() - 1; i >= 0; i--) { - if (Character.isWhitespace(value.charAt(i))) { - endIndex--; - break; - } - } - return value.substring(0, endIndex); + private void addGitInfo(Infrastructure infrastructure, GitInfoCollector git) { + git.getOriginUrl() // + .ifPresent( + gitUrl -> infrastructure.append(repository(), repository -> repository.withOriginUrl(gitUrl))); + git.getBranch() // + .ifPresent(branch -> infrastructure.append(branch(branch))); + git.getCommitHash() // + .ifPresent(gitCommitHash -> infrastructure.append(commit(gitCommitHash))); + git.getStatus() // + .ifPresent(statusOutput -> infrastructure.append(status(statusOutput), + status -> status.withClean(statusOutput.isEmpty()))); } @Override
platform-tests/src/test/java/org/junit/platform/reporting/open/xml/OpenTestReportGeneratingListenerTests.java+71 −16 modified@@ -23,6 +23,7 @@ import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; import static org.junit.platform.launcher.core.LauncherFactoryForTestingPurposesOnly.createLauncher; import static org.junit.platform.reporting.open.xml.OpenTestReportGeneratingListener.ENABLED_PROPERTY_NAME; +import static org.junit.platform.reporting.open.xml.OpenTestReportGeneratingListener.GIT_ENABLED_PROPERTY_NAME; import static org.junit.platform.reporting.testutil.FileUtils.findPath; import java.io.PrintStream; @@ -36,6 +37,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; import org.junit.platform.commons.util.ExceptionUtils; import org.junit.platform.engine.TestEngine; @@ -58,6 +60,12 @@ */ public class OpenTestReportGeneratingListenerTests { + private static final Map<String, String> NAMESPACE_CONTEXT = Map.of( // + "core", Namespace.REPORTING_CORE.getUri(), // + "e", Namespace.REPORTING_EVENTS.getUri(), // + "git", Namespace.REPORTING_GIT.getUri() // + ); + private PrintStream originalOut; private PrintStream originalErr; @@ -172,7 +180,7 @@ void writesValidXmlReport(@TempDir Path tempDirectory) throws Exception { @ParameterizedTest @ValueSource(strings = { "https://github.com/junit-team/junit-framework.git", "git@github.com:junit-team/junit-framework.git" }) - void includesGitInfo(String originUrl, @TempDir Path tempDirectory) throws Exception { + void includesGitInfoWhenEnabled(String originUrl, @TempDir Path tempDirectory) throws Exception { assumeTrue(tryExecGit(tempDirectory, "--version").exitCode() == 0, "git not installed"); execGit(tempDirectory, "init", "--initial-branch=my_branch"); @@ -192,36 +200,76 @@ void includesGitInfo(String originUrl, @TempDir Path tempDirectory) throws Excep var xmlFile = findPath(tempDirectory, "glob:**/open-test-report.xml"); assertThat(validate(xmlFile)).isEmpty(); - var namespaceContext = Map.of("core", Namespace.REPORTING_CORE.getUri(), "e", - Namespace.REPORTING_EVENTS.getUri(), "git", Namespace.REPORTING_GIT.getUri()); + assertThatXml(xmlFile) // + .doesNotHaveXPath("/e:events/core:infrastructure/git:repository"); + assertThatXml(xmlFile) // + .doesNotHaveXPath("/e:events/core:infrastructure/git:branch"); + assertThatXml(xmlFile) // + .doesNotHaveXPath("/e:events/core:infrastructure/git:commit"); + assertThatXml(xmlFile) // + .doesNotHaveXPath("/e:events/core:infrastructure/git:status"); + + executeTests(tempDirectory, engine, tempDirectory.resolve("junit-reports"), + Map.of(GIT_ENABLED_PROPERTY_NAME, "true")); + + assertThat(validate(xmlFile)).isEmpty(); - XmlAssert.assertThat(xmlFile) // - .withNamespaceContext(namespaceContext) // + assertThatXml(xmlFile) // .valueByXPath("/e:events/core:infrastructure/git:repository/@originUrl") // .isEqualTo(originUrl); - XmlAssert.assertThat(xmlFile) // - .withNamespaceContext(namespaceContext) // + assertThatXml(xmlFile) // .valueByXPath("/e:events/core:infrastructure/git:branch") // .isEqualTo("my_branch"); var commitHash = execGit(tempDirectory, "rev-parse", "--verify", "HEAD").stdOut().trim(); - XmlAssert.assertThat(xmlFile) // - .withNamespaceContext(namespaceContext) // + assertThatXml(xmlFile) // .valueByXPath("/e:events/core:infrastructure/git:commit") // .isEqualTo(commitHash); - XmlAssert.assertThat(xmlFile) // - .withNamespaceContext(namespaceContext) // + assertThatXml(xmlFile) // .valueByXPath("/e:events/core:infrastructure/git:status/@clean") // .isEqualTo(false); - XmlAssert.assertThat(xmlFile) // - .withNamespaceContext(namespaceContext) // + assertThatXml(xmlFile) // .valueByXPath("/e:events/core:infrastructure/git:status") // .startsWith("?? junit-reports"); } + @ParameterizedTest + @CsvSource(textBlock = """ + https://foo:bar@github.com/junit-team/junit5.git, https://***@github.com/junit-team/junit5.git + https://token@github.com/junit-team/junit5.git, https://***@github.com/junit-team/junit5.git + foo@github.com:junit-team/junit5.git, ***@github.com:junit-team/junit5.git + ssh://foo@github.com:junit-team/junit5.git, ssh://***@github.com:junit-team/junit5.git + git@github.com:junit-team/junit5.git, git@github.com:junit-team/junit5.git + ssh://git@github.com:junit-team/junit5.git, ssh://git@github.com:junit-team/junit5.git + """) + void stripsCredentialsFromOriginUrl(String configuredUrl, String reportedUrl, @TempDir Path tempDirectory) + throws Exception { + + assumeTrue(tryExecGit(tempDirectory, "--version").exitCode() == 0, "git not installed"); + execGit(tempDirectory, "init", "--initial-branch=my_branch"); + execGit(tempDirectory, "remote", "add", "origin", configuredUrl); + + var engine = new DemoHierarchicalTestEngine("dummy"); + + executeTests(tempDirectory, engine, tempDirectory.resolve("junit-reports"), + Map.of(GIT_ENABLED_PROPERTY_NAME, "true")); + + var xmlFile = findPath(tempDirectory, "glob:**/open-test-report.xml"); + assertThat(validate(xmlFile)).isEmpty(); + + assertThatXml(xmlFile) // + .valueByXPath("/e:events/core:infrastructure/git:repository/@originUrl") // + .isEqualTo(reportedUrl); + } + + private static XmlAssert assertThatXml(Path xmlFile) { + return XmlAssert.assertThat(xmlFile) // + .withNamespaceContext(NAMESPACE_CONTEXT); + } + private static ProcessResult execGit(Path workingDir, String... arguments) throws InterruptedException { var result = tryExecGit(workingDir, arguments); assertEquals(0, result.exitCode(), "git " + String.join(" ", arguments) + " failed"); @@ -242,15 +290,22 @@ private ValidationResult validate(Path xmlFile) throws URISyntaxException { return new DefaultValidator(catalogUri).validate(xmlFile); } - private void executeTests(Path tempDirectory, TestEngine engine, Path outputDir) { - var build = request() // + private static void executeTests(Path tempDirectory, TestEngine engine, Path outputDir) { + executeTests(tempDirectory, engine, outputDir, Map.of()); + } + + private static void executeTests(Path tempDirectory, TestEngine engine, Path outputDir, + Map<String, String> extraConfigurationParameters) { + var request = request() // .selectors(selectUniqueId(UniqueId.forEngine(engine.getId()))) // + .enableImplicitConfigurationParameters(false) // .configurationParameter(ENABLED_PROPERTY_NAME, String.valueOf(true)) // .configurationParameter(CAPTURE_STDOUT_PROPERTY_NAME, String.valueOf(true)) // .configurationParameter(CAPTURE_STDERR_PROPERTY_NAME, String.valueOf(true)) // .configurationParameter(OUTPUT_DIR_PROPERTY_NAME, outputDir.toString()) // + .configurationParameters(extraConfigurationParameters) // .build(); - createLauncher(engine).execute(build, new OpenTestReportGeneratingListener(tempDirectory)); + createLauncher(engine).execute(request, new OpenTestReportGeneratingListener(tempDirectory)); } }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-m43g-m425-p68xghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-53103ghsaADVISORY
- github.com/junit-team/junit-framework/blob/6b7764dac92fd35cb348152d1b37f8726875a4e0/junit-platform-reporting/src/main/java/org/junit/platform/reporting/open/xml/OpenTestReportGeneratingListener.javaghsaWEB
- github.com/junit-team/junit-framework/commit/d4fc834c8c1c0b3168cd030c13551d1d041f51bcnvdWEB
- github.com/junit-team/junit-framework/security/advisories/GHSA-m43g-m425-p68xnvdWEB
News mentions
0No linked articles in our index yet.