VYPR
Low severityNVD Advisory· Published May 19, 2026· Updated May 19, 2026

CVE-2026-7860

CVE-2026-7860

Description

A possible information disclosure vulnerability exists in the Vaadin Maven plugin and Vaadin Gradle plugin that exposes the full set of environment variables in build logs whenever the frontend build process exits with a non-zero status. Because the build environment may contain credentials supplied as secrets, any failed frontend build can expose those secrets in clear text in CI logs and archived build artifacts.

Users of affected versions should apply the following mitigation or upgrade. Releases that have fixed this issue include:

Product version Vaadin 23.0.0 - 23.6.9 Vaadin 24.0.0 - 24.10.3 Vaadin 25.0.0 - 25.1.4

Mitigation Upgrade to 23.6.10 Upgrade to 24.10.4 or newer Upgrade to 25.1.5 or newer

Please note that Vaadin versions 10-13 and 15-22 are no longer supported and you should update either to the latest 23, 24, or 25 version.

ArtifactsMaven coordinatesVulnerable versionsFixed versioncom.vaadin:flow-plugin-base23.0.0 - 23.6.10≥23.6.11com.vaadin:flow-plugin-base24.0.0 - 24.10.3≥24.10.4com.vaadin:flow-plugin-base25.0.0 - 25.1.4≥25.1.5com.vaadin:flow-maven-plugin23.0.0 - 23.6.10≥23.6.11com.vaadin:flow-maven-plugin24.0.0 - 24.10.3≥24.10.4com.vaadin:flow-maven-plugin25.0.0 - 25.1.4≥25.1.5com.vaadin:flow-gradle-plugin24.0.0 - 24.10.3≥24.10.4com.vaadin:flow-gradle-plugin25.0.0 - 25.1.4≥25.1.5

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Vaadin Maven and Gradle plugins expose all environment variables in build logs when frontend build fails, potentially leaking secrets.

Vulnerability

The Vaadin Maven plugin (com.vaadin:flow-maven-plugin) and Vaadin Gradle plugin (com.vaadin:flow-gradle-plugin) in affected versions expose the full set of environment variables in build logs whenever the frontend build process exits with a non-zero status [2]. Affected versions: Vaadin 23.0.0–23.6.9, 24.0.0–24.10.3, 25.0.0–25.1.4 [2]. The vulnerability is triggered only when the frontend build fails, which is uncommon in normal Vaadin application builds [2].

Exploitation

An attacker does not need direct access to the build process; the exposure occurs automatically when a frontend build failure happens. The plugin writes environment variables to standard build output, which are captured in CI logs and archived build artifacts [2]. No user interaction beyond triggering a failed frontend build is required. The attacker would need access to the logs or artifacts where the information is stored.

Impact

Successful exploitation leads to information disclosure of all environment variables, including any credentials or secrets supplied to the build environment [2]. These secrets are exposed in clear text in CI logs and any artifacts that capture build output. CI providers may mask secrets in live logs, but archived logs and shared diagnostic logs are not masked [2]. The impact is limited to confidentiality of environment variables; no code execution or privilege escalation is described.

Mitigation

Upgrade to fixed versions: Vaadin 23.6.10, 24.10.4, or 25.1.5 or newer [2]. For Maven, update com.vaadin:flow-plugin-base and com.vaadin:flow-maven-plugin to ≥23.6.11, ≥24.10.4, or ≥25.1.5; for Gradle, update com.vaadin:flow-gradle-plugin to ≥24.10.4 or ≥25.1.5 [2]. The fix replaces the zt-exec dependency with java.lang.ProcessBuilder to avoid including environment variable values in debug output [1]. Vaadin versions 10–13 and 15–22 are no longer supported and should be upgraded to a supported version [2].

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

4

Patches

1
f77f90d37c72

chore: remove `zt-exec` dependency (#24219)

https://github.com/vaadin/flowMarco CollovatiApr 30, 2026via nvd-ref
7 files changed · +101 41
  • flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/Reflector.java+2 3 modified
    @@ -57,9 +57,8 @@ public final class Reflector implements Closeable {
                 "org.apache.maven", "org.codehaus.plexus", "org.slf4j",
                 "org.eclipse.sisu");
         // Dependency required by the plugin but not provided by Flow at runtime
    -    private static final Set<String> REQUIRED_PLUGIN_DEPENDENCIES = Set.of(
    -            "io.github.classgraph:classgraph:jar",
    -            "org.zeroturnaround:zt-exec:jar");
    +    private static final Set<String> REQUIRED_PLUGIN_DEPENDENCIES = Set
    +            .of("io.github.classgraph:classgraph:jar");
         private static final ScopeArtifactFilter PRODUCTION_SCOPE_FILTER = new ScopeArtifactFilter(
                 Artifact.SCOPE_COMPILE_PLUS_RUNTIME);
         private static final Cleaner CLEANER = Cleaner.create();
    
  • flow-plugins/flow-maven-plugin/src/it/plugin-pinned-deps-project/pom.xml+0 5 modified
    @@ -39,11 +39,6 @@
                 <artifactId>classgraph</artifactId>
                 <version>4.0.0</version>
             </dependency>
    -        <dependency>
    -            <groupId>org.zeroturnaround</groupId>
    -            <artifactId>zt-exec</artifactId>
    -            <version>1.4</version>
    -        </dependency>
         </dependencies>
     
         <build>
    
  • flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildFrontendMojo.java+1 3 modified
    @@ -19,7 +19,6 @@
     import java.net.URISyntaxException;
     import java.util.List;
     import java.util.Objects;
    -import java.util.concurrent.TimeoutException;
     import java.util.function.Consumer;
     
     import org.apache.maven.artifact.Artifact;
    @@ -190,8 +189,7 @@ protected void executeInternal()
                     if (cleanFrontendFiles()) {
                         cleanTask.execute();
                     }
    -            } catch (URISyntaxException | TimeoutException
    -                    | ExecutionFailedException exception) {
    +            } catch (URISyntaxException | ExecutionFailedException exception) {
                     throw new MojoExecutionException(exception.getMessage(),
                             exception);
                 }
    
  • flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/Reflector.java+2 3 modified
    @@ -70,9 +70,8 @@ public final class Reflector implements Closeable {
                 "org.apache.maven", "org.codehaus.plexus", "org.slf4j",
                 "org.eclipse.sisu");
         // Dependency required by the plugin but not provided by Flow at runtime
    -    private static final Set<String> REQUIRED_PLUGIN_DEPENDENCIES = Set.of(
    -            "io.github.classgraph:classgraph:jar",
    -            "org.zeroturnaround:zt-exec:jar");
    +    private static final Set<String> REQUIRED_PLUGIN_DEPENDENCIES = Set
    +            .of("io.github.classgraph:classgraph:jar");
         private static final ScopeArtifactFilter PRODUCTION_SCOPE_FILTER = new ScopeArtifactFilter(
                 Artifact.SCOPE_COMPILE_PLUS_RUNTIME);
         private static final Cleaner CLEANER = Cleaner.create();
    
  • flow-plugins/flow-plugin-base/pom.xml+0 5 modified
    @@ -34,11 +34,6 @@
           <artifactId>classgraph</artifactId>
           <version>4.8.184</version>
         </dependency>
    -    <dependency>
    -      <groupId>org.zeroturnaround</groupId>
    -      <artifactId>zt-exec</artifactId>
    -      <version>1.12</version>
    -    </dependency>
         <dependency>
           <groupId>jakarta.servlet</groupId>
           <artifactId>jakarta.servlet-api</artifactId>
    
  • flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java+58 22 modified
    @@ -15,6 +15,7 @@
      */
     package com.vaadin.flow.plugin.base;
     
    +import java.io.BufferedReader;
     import java.io.File;
     import java.io.IOException;
     import java.io.UncheckedIOException;
    @@ -30,7 +31,6 @@
     import java.util.List;
     import java.util.Map;
     import java.util.Set;
    -import java.util.concurrent.TimeoutException;
     import java.util.jar.Attributes;
     import java.util.jar.JarFile;
     import java.util.jar.Manifest;
    @@ -40,8 +40,6 @@
     import org.apache.commons.io.IOUtils;
     import org.slf4j.Logger;
     import org.slf4j.LoggerFactory;
    -import org.zeroturnaround.exec.InvalidExitValueException;
    -import org.zeroturnaround.exec.ProcessExecutor;
     import tools.jackson.databind.JsonNode;
     import tools.jackson.databind.node.ObjectNode;
     
    @@ -499,13 +497,11 @@ public static File getGeneratedFrontendDirectory(
          *
          * @param adapter
          *            - the PluginAdapterBase.
    -     * @throws TimeoutException
    -     *             - while running build system
          * @throws URISyntaxException
          *             - while parsing nodeDownloadRoot()) to URI
          */
         public static void runFrontendBuild(PluginAdapterBase adapter)
    -            throws TimeoutException, URISyntaxException {
    +            throws URISyntaxException {
             LicenseChecker.setStrictOffline(true);
     
             FrontendToolsSettings settings = getFrontendToolsSettings(adapter);
    @@ -540,19 +536,17 @@ public static void runFrontendBuild(PluginAdapterBase adapter)
          *            - the PluginAdapterBase.
          * @param frontendTools
          *            - frontend tools access object
    -     * @throws TimeoutException
    -     *             - while running vite
          */
         public static void runVite(PluginAdapterBase adapter,
    -            FrontendTools frontendTools) throws TimeoutException {
    +            FrontendTools frontendTools) {
             runFrontendBuildTool(adapter, frontendTools, "Vite", "vite", "vite",
                     Collections.emptyMap(), "build");
         }
     
         private static void runFrontendBuildTool(PluginAdapterBase adapter,
                 FrontendTools frontendTools, String toolName, String packageName,
                 String binaryName, Map<String, String> environment,
    -            String... params) throws TimeoutException {
    +            String... params) {
     
             File buildExecutable;
             try {
    @@ -580,28 +574,70 @@ private static void runFrontendBuildTool(PluginAdapterBase adapter,
             command.addAll(Arrays.asList(params));
     
             ProcessBuilder builder = FrontendUtils.createProcessBuilder(command);
    -
    -        ProcessExecutor processExecutor = new ProcessExecutor()
    -                .command(builder.command()).environment(builder.environment())
    -                .environment(environment)
    -                .directory(adapter.projectBaseDirectory().toFile());
    +        if (!environment.isEmpty()) {
    +            builder.environment().putAll(environment);
    +        }
    +        builder.directory(adapter.projectBaseDirectory().toFile());
    +        builder.redirectErrorStream(true);
     
             adapter.logInfo("Running " + toolName + " ...");
             if (adapter.isDebugEnabled()) {
                 adapter.logDebug(FrontendUtils.commandToString(
                         adapter.npmFolder().getAbsolutePath(), command));
             }
    +
    +        Process process = null;
    +        // Per-invocation hook: registered before start, removed in finally.
    +        // The unconditional add+forget pattern leaks Thread refs in the Gradle
    +        // daemon JVM, where the hook list never drains until JVM shutdown.
    +        Thread shutdownHook = null;
             try {
    -            processExecutor.exitValueNormal().readOutput(true).destroyOnExit()
    -                    .execute();
    -        } catch (InvalidExitValueException e) {
    -            throw new IllegalStateException(String.format(
    -                    "%s process exited with non-zero exit code.%nStderr: '%s'",
    -                    toolName, e.getResult().outputUTF8()), e);
    -        } catch (IOException | InterruptedException e) {
    +            process = builder.start();
    +            final Process startedProcess = process;
    +            shutdownHook = new Thread(() -> {
    +                if (startedProcess.isAlive()) {
    +                    startedProcess.destroyForcibly();
    +                }
    +            }, toolName + "-shutdown-hook");
    +            Runtime.getRuntime().addShutdownHook(shutdownHook);
    +
    +            StringBuilder toolOutput = new StringBuilder();
    +            try (BufferedReader reader = process
    +                    .inputReader(StandardCharsets.UTF_8)) {
    +                reader.lines().forEach(line -> {
    +                    if (adapter.isDebugEnabled()) {
    +                        adapter.logDebug(line);
    +                    }
    +                    toolOutput.append(line).append(System.lineSeparator());
    +                });
    +            }
    +
    +            int exitCode = process.waitFor();
    +            if (exitCode != 0) {
    +                throw new IllegalStateException(String.format(
    +                        "%s process exited with non-zero exit code %d.%nOutput: '%s'",
    +                        toolName, exitCode, toolOutput.toString().trim()));
    +            }
    +        } catch (IOException e) {
    +            throw new IllegalStateException(
    +                    String.format("Failed to run %s due to an error", toolName),
    +                    e);
    +        } catch (InterruptedException e) {
    +            Thread.currentThread().interrupt();
                 throw new IllegalStateException(
                         String.format("Failed to run %s due to an error", toolName),
                         e);
    +        } finally {
    +            if (process != null && process.isAlive()) {
    +                process.destroyForcibly();
    +            }
    +            if (shutdownHook != null) {
    +                try {
    +                    Runtime.getRuntime().removeShutdownHook(shutdownHook);
    +                } catch (IllegalStateException ignore) {
    +                    // JVM is already shutting down; the hook will run.
    +                }
    +            }
             }
         }
     
    
  • flow-plugins/flow-plugin-base/src/test/java/com/vaadin/flow/plugin/base/BuildFrontendUtilTest.java+38 0 modified
    @@ -39,6 +39,8 @@
     import org.apache.commons.io.IOUtils;
     import org.junit.jupiter.api.BeforeEach;
     import org.junit.jupiter.api.Test;
    +import org.junit.jupiter.api.condition.DisabledOnOs;
    +import org.junit.jupiter.api.condition.OS;
     import org.junit.jupiter.api.io.TempDir;
     import org.mockito.Answers;
     import org.mockito.InOrder;
    @@ -877,6 +879,42 @@ private void addPremiumFeatureAndDAUFlagTrue(File tokenFile)
                     buildInfo.toPrettyString() + "\n");
         }
     
    +    @Test
    +    @DisabledOnOs(OS.WINDOWS)
    +    void runVite_nonZeroExit_doesNotLeakEnvironment() throws Exception {
    +        File fakeNode = new File(baseDir, "fake-node.sh");
    +        Files.writeString(fakeNode.toPath(), """
    +                #!/bin/sh
    +                echo TEST_OUTPUT_LINE
    +                exit 7
    +                """);
    +        assertTrue(fakeNode.setExecutable(true));
    +
    +        File viteJs = new File(baseDir, "node_modules/vite/bin/vite.js");
    +
    +        FrontendTools frontendTools = Mockito.mock(FrontendTools.class);
    +        Mockito.when(frontendTools.getNodeExecutable())
    +                .thenReturn(fakeNode.getAbsolutePath());
    +        Mockito.when(
    +                frontendTools.getNpmPackageExecutable("vite", "vite", baseDir))
    +                .thenReturn(viteJs.toPath());
    +
    +        IllegalStateException ex = assertThrows(IllegalStateException.class,
    +                () -> BuildFrontendUtil.runVite(adapter, frontendTools));
    +
    +        String message = ex.getMessage();
    +        assertTrue(message.contains("exited with non-zero exit code 7"),
    +                () -> "missing exit code in: " + message);
    +        assertTrue(message.contains("TEST_OUTPUT_LINE"),
    +                () -> "missing process output in: " + message);
    +        // zt-exec exception messages started with "with environment {…}" —
    +        // proving its absence proves we are off the leak path.
    +        assertFalse(message.contains("with environment"),
    +                () -> "leaked env marker in: " + message);
    +        // No zt-exec exception should be chained as cause.
    +        assertEquals(null, ex.getCause());
    +    }
    +
         private static String statsJsonWithCommercialComponents() {
             return """
                         {
    

Vulnerability mechanics

Synthesis attempt was rejected by the grounding validator. Re-run pending.

References

2

News mentions

0

No linked articles in our index yet.