Skip to content

Commit

Permalink
fix: validate downloaded node (#20821)
Browse files Browse the repository at this point in the history
Validate downloaded node
against the provided
sha256 hash.

Fixes #20661
  • Loading branch information
caalador authored Jan 10, 2025
1 parent 07d230e commit fb0862f
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

Expand Down Expand Up @@ -75,6 +76,39 @@ public static byte[] sha256(String string, byte[] salt, Charset charset) {
return getSha256(salt).digest(string.getBytes(charset));
}

/**
* Calculates the SHA-256 hash of the given byte array.
*
* @param content
* the byte array to hash
*
* @return sha256 hash string
*/
public static String sha256Hex(byte[] content) {
return sha256Hex(content, null);
}

/**
* Calculates the SHA-256 hash of the given byte array with the given salt.
*
* @param content
* the byte array to hash
* @param salt
* salt to be added to the calculation
* @return sha256 hash string
*/
public static String sha256Hex(byte[] content, byte[] salt) {
byte[] digest = getSha256(salt).digest(content);
final StringBuilder hexString = new StringBuilder();
for (int i = 0; i < digest.length; i++) {
final String hex = Integer.toHexString(0xff & digest[i]);
if (hex.length() == 1)
hexString.append('0');
hexString.append(hex);
}
return hexString.toString();
}

private static MessageDigest getSha256(byte[] salt) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.vaadin.flow.internal.MessageDigestUtil;
import com.vaadin.flow.internal.Pair;
import com.vaadin.flow.server.frontend.FrontendUtils;
import com.vaadin.flow.server.frontend.FrontendVersion;
Expand All @@ -55,6 +56,8 @@ public class NodeInstaller {

public static final String UNOFFICIAL_NODEJS_DOWNLOAD_ROOT = "https://unofficial-builds.nodejs.org/download/release/";

public static final String SHA_SUMS_FILE = "SHASUMS256.txt";

private static final String NODE_WINDOWS = INSTALL_PATH.replaceAll("/",
"\\\\") + "\\node.exe";
private static final String NODE_DEFAULT = INSTALL_PATH + "/node";
Expand All @@ -64,6 +67,7 @@ public class NodeInstaller {
private static final int MAX_DOWNLOAD_ATTEMPS = 5;

private static final int DOWNLOAD_ATTEMPT_DELAY = 5;
public static final String ACCEPT_MISSING_SHA = "vaadin.node.download.acceptMissingSHA";

private final Object lock = new Object();

Expand Down Expand Up @@ -524,6 +528,8 @@ private void downloadFileIfMissing(URI downloadUrl, File destination,
try {
fileDownloader.download(downloadUrl, destination, userName,
password, null);

verifyArchive(destination);
return;
} catch (DownloadException e) {
if (i == MAX_DOWNLOAD_ATTEMPS - 1) {
Expand All @@ -538,10 +544,85 @@ private void downloadFileIfMissing(URI downloadUrl, File destination,
try {
Thread.sleep(DOWNLOAD_ATTEMPT_DELAY * 1000);
} catch (InterruptedException e1) {
Thread.currentThread().interrupt();
}
} catch (VerificationException ve) {
getLogger().warn(
"SHA256 verification of downloaded node archive failed.");
if (i == MAX_DOWNLOAD_ATTEMPS - 1) {
removeArchiveFile(destination);
throw new DownloadException(
"Failed to download node matching SHA256.");
}
}
}
} else {
try {
verifyArchive(destination);
} catch (VerificationException de) {
removeArchiveFile(destination);
downloadFileIfMissing(downloadUrl, destination, userName,
password);
}
}
}

private void verifyArchive(File archive)
throws DownloadException, VerificationException {
try {
URI shaSumsURL = nodeDownloadRoot
.resolve(nodeVersion + "/" + SHA_SUMS_FILE);
if ("file".equalsIgnoreCase(shaSumsURL.getScheme())) {
// The file is local so it can't be expected to have a SHA file
return;
}

File shaSums = new File(installDirectory, "node-" + SHA_SUMS_FILE);

getLogger().debug("Downloading {} to {}", shaSumsURL, shaSums);

try {
fileDownloader.download(shaSumsURL, shaSums, userName, password,
null);
} catch (DownloadException e) {
if (Boolean.getBoolean(ACCEPT_MISSING_SHA)) {
getLogger().warn(
"Could not verify SHA256 sum of downloaded node in {}. Accepting missing checksum verification as set in '{}' system property.",
archive, ACCEPT_MISSING_SHA);
return;
} else {
getLogger().info(
"Download of {} failed. If failure persists, use system property '{}' to skip verification or download node manually.",
SHA_SUMS_FILE, ACCEPT_MISSING_SHA);
throw e;
}
}

String archiveSHA256 = MessageDigestUtil
.sha256Hex(Files.readAllBytes(archive.toPath()));

List<String> sha256sums = Files.readAllLines(shaSums.toPath());
String archiveTargetSHA256 = sha256sums.stream()
.filter(sum -> sum
.endsWith(archive.getName()))
.map(sum -> sum
.substring(0,
sum.length() - archive.getName().length())
.trim())
.findFirst().orElse("-1");

shaSums.delete();

if (!archiveSHA256.equals(archiveTargetSHA256)) {
getLogger().error(
"Expected SHA256 [{}] for downloaded node archive, got [{}]",
archiveTargetSHA256, archiveSHA256);
throw new VerificationException(
"SHA256 sums did not match for downloaded node");
}
} catch (IOException e) {
throw new VerificationException("Failed to validate archive hash.",
e);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ public Proxy getProxyForUrl(String requestUrl) {
+ "development-mode/node-js#proxy-settings-for-downloading-"
+ "frontend-toolchain for information on proxy configuration.";
if (proxies.isEmpty()) {
getLogger().info("No proxies configured. "
+ "If you are behind a proxy server, " + docLink);
getLogger().debug(
"No proxies configured. If you are behind a proxy server, {}",
docLink);
return null;
}
final URI uri = URI.create(requestUrl);
Expand All @@ -80,9 +81,8 @@ public Proxy getProxyForUrl(String requestUrl) {
return proxy;
}
}
getLogger().info(
"Could not find matching proxy for host: {}" + " - " + docLink,
uri.getHost());
getLogger().info("Could not find matching proxy for host: {} - {}",
uri.getHost(), docLink);
return null;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2000-2024 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.flow.server.frontend.installer;

/**
* Exception indicating a failure during downloaded archive verification.
* <p>
* For internal use only. May be renamed or removed in a future release.
*
* @since
*/
public final class VerificationException extends Exception {

/**
* Exceptioon with message.
*
* @param message
* exception message
*/
public VerificationException(String message) {
super(message);
}

/**
* Exceptioon with message and cause.
*
* @param message
* exception message
* @param cause
* cause for exception
*/
VerificationException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ public void nodeIsBeingLocated_supportedNodeInstalled_autoUpdateTrue_NodeUpdated
@Test
public void nodeIsBeingLocated_unsupportedNodeInstalled_defaultNodeVersionInstalledToAlternativeDirectory()
throws FrontendUtils.UnknownVersionException, IOException {
Assume.assumeFalse(
"Skipping test on windows until a fake node.exe that isn't caught by Window defender can be created.",
FrontendUtils.isWindows());
// Unsupported node version
FrontendStubs.ToolStubInfo nodeStub = FrontendStubs.ToolStubInfo
.builder(FrontendStubs.Tool.NODE).withVersion("8.9.3").build();
Expand All @@ -204,6 +207,9 @@ public void nodeIsBeingLocated_unsupportedNodeInstalled_defaultNodeVersionInstal
@Test
public void nodeIsBeingLocated_unsupportedNodeInstalled_fallbackToNodeInstalledToAlternativeDirectory()
throws IOException, FrontendUtils.UnknownVersionException {
Assume.assumeFalse(
"Skipping test on windows until a fake node.exe that isn't caught by Window defender can be created.",
FrontendUtils.isWindows());
// Unsupported node version
FrontendStubs.ToolStubInfo nodeStub = FrontendStubs.ToolStubInfo
.builder(FrontendStubs.Tool.NODE).withVersion("8.9.3").build();
Expand Down Expand Up @@ -730,6 +736,8 @@ public void getSuitablePnpm_supportedGlobalVersionInstalled_accepted() {

@Test
public void getSuitablePnpm_useGlobalPnpm_noPnpmInstalled_throws() {
Assume.assumeFalse("Skipping test on windows.",
FrontendUtils.isWindows());
Optional<File> pnpm = frontendToolsLocator.tryLocateTool("pnpm");
Assume.assumeFalse("Skip this test once globally installed pnpm is "
+ "discovered", pnpm.isPresent());
Expand Down

0 comments on commit fb0862f

Please sign in to comment.