Skip to content

Commit 957e2f8

Browse files
committed
Add warning if sensitive container paths are bound
Closes gh-41643
1 parent 35361d1 commit 957e2f8

File tree

6 files changed

+149
-2
lines changed

6 files changed

+149
-2
lines changed

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/AbstractBuildLog.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent;
2323
import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent;
24+
import org.springframework.boot.buildpack.platform.docker.type.Binding;
2425
import org.springframework.boot.buildpack.platform.docker.type.Image;
2526
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
2627
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
@@ -118,6 +119,13 @@ public void failedCleaningWorkDir(Cache cache, Exception exception) {
118119
log();
119120
}
120121

122+
@Override
123+
public void sensitiveTargetBindingDetected(Binding binding) {
124+
log("Warning: Binding '%s' uses a container path which is used by buildpacks while building. Binding to it can cause problems!"
125+
.formatted(binding));
126+
log();
127+
}
128+
121129
private String getDigest(Image image) {
122130
List<String> digests = image.getDigests();
123131
return (digests.isEmpty() ? "" : digests.get(0));

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildLog.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent;
2323
import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent;
24+
import org.springframework.boot.buildpack.platform.docker.type.Binding;
2425
import org.springframework.boot.buildpack.platform.docker.type.Image;
2526
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
2627
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
@@ -125,6 +126,13 @@ Consumer<TotalProgressEvent> pullingImage(ImageReference imageReference, ImagePl
125126
*/
126127
void failedCleaningWorkDir(Cache cache, Exception exception);
127128

129+
/**
130+
* Log that a binding with a sensitive target has been detected.
131+
* @param binding the binding
132+
* @since 3.4.0
133+
*/
134+
void sensitiveTargetBindingDetected(Binding binding);
135+
128136
/**
129137
* Factory method that returns a {@link BuildLog} the outputs to {@link System#out}.
130138
* @return a build log instance that logs to system out

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
2929
import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost;
3030
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
31+
import org.springframework.boot.buildpack.platform.docker.type.Binding;
3132
import org.springframework.boot.buildpack.platform.docker.type.Image;
3233
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
3334
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
@@ -98,6 +99,7 @@ public Builder(BuildLog log, DockerConfiguration dockerConfiguration) {
9899
public void build(BuildRequest request) throws DockerEngineException, IOException {
99100
Assert.notNull(request, "Request must not be null");
100101
this.log.start(request);
102+
validateBindings(request.getBindings());
101103
String domain = request.getBuilder().getDomain();
102104
PullPolicy pullPolicy = request.getPullPolicy();
103105
ImageFetcher imageFetcher = new ImageFetcher(domain, getBuilderAuthHeader(), pullPolicy,
@@ -125,6 +127,14 @@ public void build(BuildRequest request) throws DockerEngineException, IOExceptio
125127
}
126128
}
127129

130+
private void validateBindings(List<Binding> bindings) {
131+
for (Binding binding : bindings) {
132+
if (binding.usesSensitiveContainerPath()) {
133+
this.log.sensitiveTargetBindingDetected(binding);
134+
}
135+
}
136+
}
137+
128138
private BuildRequest withRunImageIfNeeded(BuildRequest request, BuilderMetadata metadata) {
129139
if (request.getRunImage() != null) {
130140
return request;

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Binding.java

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2022 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,18 +16,28 @@
1616

1717
package org.springframework.boot.buildpack.platform.docker.type;
1818

19+
import java.util.ArrayList;
20+
import java.util.List;
1921
import java.util.Objects;
22+
import java.util.Set;
2023

2124
import org.springframework.util.Assert;
2225

2326
/**
2427
* Volume bindings to apply when creating a container.
2528
*
2629
* @author Scott Frederick
30+
* @author Moritz Halbritter
2731
* @since 2.5.0
2832
*/
2933
public final class Binding {
3034

35+
/**
36+
* Sensitive container paths, which lead to problems if used in a binding.
37+
*/
38+
private static final Set<String> SENSITIVE_CONTAINER_PATHS = Set.of("/cnb", "/layers", "/workspace", "c:\\cnb",
39+
"c:\\layers", "c:\\workspace");
40+
3141
private final String value;
3242

3343
private Binding(String value) {
@@ -55,6 +65,45 @@ public String toString() {
5565
return this.value;
5666
}
5767

68+
/**
69+
* Returns the container destination path.
70+
* @return the container destination path
71+
*/
72+
String getContainerDestinationPath() {
73+
List<String> parts = split(this.value, ':', '\\');
74+
// Format is <host>:<container>:[<options>]
75+
Assert.state(parts.size() >= 2, () -> "Expected 2 or more parts, but found %d".formatted(parts.size()));
76+
return parts.get(1);
77+
}
78+
79+
private List<String> split(String input, char delimiter, char notFollowedBy) {
80+
Assert.state(notFollowedBy != '\0', "notFollowedBy must not be the null terminator");
81+
List<String> parts = new ArrayList<>();
82+
StringBuilder accumulator = new StringBuilder();
83+
for (int i = 0; i < input.length(); i++) {
84+
char c = input.charAt(i);
85+
char nextChar = (i + 1 < input.length()) ? input.charAt(i + 1) : '\0';
86+
if (c == delimiter && nextChar != notFollowedBy) {
87+
parts.add(accumulator.toString());
88+
accumulator.setLength(0);
89+
}
90+
else {
91+
accumulator.append(c);
92+
}
93+
}
94+
parts.add(accumulator.toString());
95+
return parts;
96+
}
97+
98+
/**
99+
* Whether the binding uses a sensitive container path.
100+
* @return whether the binding uses a sensitive container path
101+
* @since 3.4.0
102+
*/
103+
public boolean usesSensitiveContainerPath() {
104+
return SENSITIVE_CONTAINER_PATHS.contains(getContainerDestinationPath());
105+
}
106+
58107
/**
59108
* Create a {@link Binding} with the specified value containing a host source,
60109
* container destination, and options.

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener;
3333
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
3434
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
35+
import org.springframework.boot.buildpack.platform.docker.type.Binding;
3536
import org.springframework.boot.buildpack.platform.docker.type.ContainerReference;
3637
import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus;
3738
import org.springframework.boot.buildpack.platform.docker.type.Image;
@@ -521,6 +522,26 @@ void buildWhenRequestedBuildpackNotInBuilderThrowsException() throws Exception {
521522
.withMessageContaining("not found in builder");
522523
}
523524

525+
@Test
526+
void logsWarningIfBindingWithSensitiveTargetIsDetected() throws IOException {
527+
TestPrintStream out = new TestPrintStream();
528+
DockerApi docker = mockDockerApi();
529+
Image builderImage = loadImage("image.json");
530+
Image runImage = loadImage("run-image.json");
531+
given(docker.image()
532+
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull()))
533+
.willAnswer(withPulledImage(builderImage));
534+
given(docker.image()
535+
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
536+
any(), isNull()))
537+
.willAnswer(withPulledImage(runImage));
538+
Builder builder = new Builder(BuildLog.to(out), docker, null);
539+
BuildRequest request = getTestRequest().withBindings(Binding.from("/host", "/cnb"));
540+
builder.build(request);
541+
assertThat(out.toString()).contains(
542+
"Warning: Binding '/host:/cnb' uses a container path which is used by buildpacks while building. Binding to it can cause problems!");
543+
}
544+
524545
private DockerApi mockDockerApi() throws IOException {
525546
return mockDockerApi(null);
526547
}

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/BindingTests.java

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,14 +17,18 @@
1717
package org.springframework.boot.buildpack.platform.docker.type;
1818

1919
import org.junit.jupiter.api.Test;
20+
import org.junit.jupiter.params.ParameterizedTest;
21+
import org.junit.jupiter.params.provider.CsvSource;
2022

2123
import static org.assertj.core.api.Assertions.assertThat;
2224
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
25+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
2326

2427
/**
2528
* Tests for {@link Binding}.
2629
*
2730
* @author Scott Frederick
31+
* @author Moritz Halbritter
2832
*/
2933
class BindingTests {
3034

@@ -70,4 +74,51 @@ void fromVolumeNameSourceWithNullSourceThrowsException() {
7074
.withMessageContaining("SourceVolume must not be null");
7175
}
7276

77+
@Test
78+
void shouldReturnContainerDestinationPath() {
79+
Binding binding = Binding.from("/host", "/container");
80+
assertThat(binding.getContainerDestinationPath()).isEqualTo("/container");
81+
}
82+
83+
@Test
84+
void shouldReturnContainerDestinationPathWithOptions() {
85+
Binding binding = Binding.of("/host:/container:ro");
86+
assertThat(binding.getContainerDestinationPath()).isEqualTo("/container");
87+
}
88+
89+
@Test
90+
void shouldReturnContainerDestinationPathOnWindows() {
91+
Binding binding = Binding.from("C:\\host", "C:\\container");
92+
assertThat(binding.getContainerDestinationPath()).isEqualTo("C:\\container");
93+
}
94+
95+
@Test
96+
void shouldReturnContainerDestinationPathOnWindowsWithOptions() {
97+
Binding binding = Binding.of("C:\\host:C:\\container:ro");
98+
assertThat(binding.getContainerDestinationPath()).isEqualTo("C:\\container");
99+
}
100+
101+
@Test
102+
void shouldFailIfBindingIsMalformed() {
103+
Binding binding = Binding.of("some-invalid-binding");
104+
assertThatIllegalStateException().isThrownBy(binding::getContainerDestinationPath)
105+
.withMessage("Expected 2 or more parts, but found 1");
106+
}
107+
108+
@ParameterizedTest
109+
@CsvSource(textBlock = """
110+
/cnb, true
111+
/layers, true
112+
/workspace, true
113+
/something, false
114+
c:\\cnb, true
115+
c:\\layers, true
116+
c:\\workspace, true
117+
c:\\something, false
118+
""")
119+
void shouldDetectSensitiveContainerPaths(String containerPath, boolean sensitive) {
120+
Binding binding = Binding.from("/host", containerPath);
121+
assertThat(binding.usesSensitiveContainerPath()).isEqualTo(sensitive);
122+
}
123+
73124
}

0 commit comments

Comments
 (0)