Skip to content

Commit ace0de3

Browse files
committed
[SECURITY-2422][SECURITY-2441][SECURITY-2463][SECURITY-2476][SECURITY-2479][SECURITY-2586]
1 parent d1912e9 commit ace0de3

File tree

11 files changed

+388
-33
lines changed

11 files changed

+388
-33
lines changed

src/main/java/org/jenkinsci/plugins/workflow/libs/FolderLibraries.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@ private Collection<LibraryConfiguration> forGroup(@CheckForNull ItemGroup<?> gro
7777
if (!checkPermission || f.hasPermission(Item.CONFIGURE)) {
7878
FolderLibraries prop = f.getProperties().get(FolderLibraries.class);
7979
if (prop != null) {
80-
libraries.addAll(prop.getLibraries());
80+
String source = FolderLibraries.ForJob.class.getName() + " " + f.getFullName();
81+
for (LibraryConfiguration library : prop.getLibraries()) {
82+
libraries.add(new ResolvedLibraryConfiguration(library, source));
83+
}
8184
}
8285
}
8386
}

src/main/java/org/jenkinsci/plugins/workflow/libs/LibraryAdder.java

+25-13
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
if (action != null) {
9090
// Resuming a build, so just look up what we loaded before.
9191
for (LibraryRecord record : action.getLibraries()) {
92-
FilePath libDir = new FilePath(execution.getOwner().getRootDir()).child("libs/" + record.name);
92+
FilePath libDir = new FilePath(execution.getOwner().getRootDir()).child("libs/" + record.getDirectoryName());
9393
for (String root : new String[] {"src", "vars"}) {
9494
FilePath dir = libDir.child(root);
9595
if (dir.isDirectory()) {
@@ -120,7 +120,11 @@
120120
}
121121
String version = cfg.defaultedVersion(libraryVersions.remove(name));
122122
Boolean changelog = cfg.defaultedChangelogs(libraryChangelogs.remove(name));
123-
librariesAdded.put(name, new LibraryRecord(name, version, kindTrusted, changelog, cfg.getCachingConfiguration()));
123+
String source = kind.getClass().getName();
124+
if (cfg instanceof LibraryResolver.ResolvedLibraryConfiguration) {
125+
source = ((LibraryResolver.ResolvedLibraryConfiguration) cfg).getSource();
126+
}
127+
librariesAdded.put(name, new LibraryRecord(name, version, kindTrusted, changelog, cfg.getCachingConfiguration(), source));
124128
retrievers.put(name, cfg.getRetriever());
125129
}
126130
}
@@ -135,7 +139,7 @@
135139
// Now actually try to retrieve the libraries.
136140
for (LibraryRecord record : librariesAdded.values()) {
137141
listener.getLogger().println("Loading library " + record.name + "@" + record.version);
138-
for (URL u : retrieve(record.name, record.version, retrievers.get(record.name), record.trusted, record.changelog, record.cachingConfiguration, listener, build, execution, record.variables)) {
142+
for (URL u : retrieve(record, retrievers.get(record.name), listener, build, execution)) {
139143
additions.add(new Addition(u, record.trusted));
140144
}
141145
}
@@ -152,11 +156,14 @@
152156
}
153157

154158
/** Retrieve library files. */
155-
static List<URL> retrieve(@NonNull String name, @NonNull String version, @NonNull LibraryRetriever retriever, boolean trusted, Boolean changelog, LibraryCachingConfiguration cachingConfiguration, @NonNull TaskListener listener, @NonNull Run<?,?> run, @NonNull CpsFlowExecution execution, @NonNull Set<String> variables) throws Exception {
156-
FilePath libDir = new FilePath(execution.getOwner().getRootDir()).child("libs/" + name);
159+
static List<URL> retrieve(@NonNull LibraryRecord record, @NonNull LibraryRetriever retriever, @NonNull TaskListener listener, @NonNull Run<?,?> run, @NonNull CpsFlowExecution execution) throws Exception {
160+
String name = record.name;
161+
String version = record.version;
162+
boolean changelog = record.changelog;
163+
LibraryCachingConfiguration cachingConfiguration = record.cachingConfiguration;
164+
FilePath libDir = new FilePath(execution.getOwner().getRootDir()).child("libs/" + record.getDirectoryName());
157165
Boolean shouldCache = cachingConfiguration != null;
158-
final FilePath libraryCacheDir = new FilePath(LibraryCachingConfiguration.getGlobalLibrariesCacheDir(), name);
159-
final FilePath versionCacheDir = new FilePath(libraryCacheDir, version);
166+
final FilePath versionCacheDir = new FilePath(LibraryCachingConfiguration.getGlobalLibrariesCacheDir(), record.getDirectoryName());
160167
final FilePath retrieveLockFile = new FilePath(versionCacheDir, LibraryCachingConfiguration.RETRIEVE_LOCK_FILE);
161168
final FilePath lastReadFile = new FilePath(versionCacheDir, LibraryCachingConfiguration.LAST_READ_FILE);
162169

@@ -195,9 +202,11 @@ static List<URL> retrieve(@NonNull String name, @NonNull String version, @NonNul
195202
} else {
196203
retriever.retrieve(name, version, changelog, libDir, run, listener);
197204
}
205+
// Write the user-provided name to a file as a debugging aid.
206+
libDir.withSuffix("-name.txt").write(name, "UTF-8");
198207

199208
// Replace any classes requested for replay:
200-
if (!trusted) {
209+
if (!record.trusted) {
201210
for (String clazz : ReplayAction.replacementsIn(execution)) {
202211
for (String root : new String[] {"src", "vars"}) {
203212
String rel = root + "/" + clazz.replace('.', '/') + ".groovy";
@@ -221,7 +230,7 @@ static List<URL> retrieve(@NonNull String name, @NonNull String version, @NonNul
221230
if (varsDir.isDirectory()) {
222231
urls.add(varsDir.toURI().toURL());
223232
for (FilePath var : varsDir.list("*.groovy")) {
224-
variables.add(var.getBaseName());
233+
record.variables.add(var.getBaseName());
225234
}
226235
}
227236
if (urls.isEmpty()) {
@@ -245,8 +254,11 @@ static List<URL> retrieve(@NonNull String name, @NonNull String version, @NonNul
245254
if (action != null) {
246255
FilePath libs = new FilePath(run.getRootDir()).child("libs");
247256
for (LibraryRecord library : action.getLibraries()) {
248-
FilePath f = libs.child(library.name + "/resources/" + name);
249-
if (f.exists()) {
257+
FilePath libResources = libs.child(library.getDirectoryName() + "/resources/");
258+
FilePath f = libResources.child(name);
259+
if (!new File(f.getRemote()).getCanonicalFile().toPath().startsWith(libResources.absolutize().getRemote())) {
260+
throw new AbortException(name + " references a file that is not contained within the library: " + library.name);
261+
} else if (f.exists()) {
250262
resources.put(library.name, readResource(f, encoding));
251263
}
252264
}
@@ -278,7 +290,7 @@ private static String readResource(FilePath file, @CheckForNull String encoding)
278290
List<GlobalVariable> vars = new ArrayList<>();
279291
for (LibraryRecord library : action.getLibraries()) {
280292
for (String variable : library.variables) {
281-
vars.add(new UserDefinedGlobalVariable(variable, new File(run.getRootDir(), "libs/" + library.name + "/vars/" + variable + ".txt")));
293+
vars.add(new UserDefinedGlobalVariable(variable, new File(run.getRootDir(), "libs/" + library.getDirectoryName() + "/vars/" + variable + ".txt")));
282294
}
283295
}
284296
return vars;
@@ -304,7 +316,7 @@ private static String readResource(FilePath file, @CheckForNull String encoding)
304316
continue; // TODO JENKINS-41157 allow replay of trusted libraries if you have ADMINISTER
305317
}
306318
for (String rootName : new String[] {"src", "vars"}) {
307-
FilePath root = libs.child(library.name + "/" + rootName);
319+
FilePath root = libs.child(library.getDirectoryName() + "/" + rootName);
308320
if (!root.isDirectory()) {
309321
continue;
310322
}

src/main/java/org/jenkinsci/plugins/workflow/libs/LibraryRecord.java

+43-2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.Collections;
2828
import java.util.Set;
2929
import java.util.TreeSet;
30+
import jenkins.security.HMACConfidentialKey;
3031
import org.kohsuke.stapler.export.Exported;
3132
import org.kohsuke.stapler.export.ExportedBean;
3233

@@ -36,26 +37,49 @@
3637
@ExportedBean
3738
public final class LibraryRecord {
3839

40+
private static final HMACConfidentialKey DIRECTORY_NAME_KEY = new HMACConfidentialKey(LibraryRecord.class, "directoryName", 32);
41+
private static final String ASCII_UNIT_SEPARATOR = String.valueOf((char)31);
42+
3943
final String name;
4044
final String version;
4145
final Set<String> variables = new TreeSet<>();
4246
final boolean trusted;
4347
final boolean changelog;
4448
final LibraryCachingConfiguration cachingConfiguration;
49+
private String directoryName;
4550

46-
LibraryRecord(String name, String version, boolean trusted, boolean changelog, LibraryCachingConfiguration cachingConfiguration) {
51+
/**
52+
* @param name The name of the library, as entered by the user. Not validated or restricted in any way.
53+
* @param version The version of the library, as entered by the user. Not validated or restricted in any way.
54+
* @param trusted Whether the library is trusted. Typically determined by {@link LibraryResolver#isTrusted}, but see also {@link LibraryStep}.
55+
* @param changelog Whether we should include any SCM changes in this library in the build's changelog.
56+
* @param cachingConfiguration If non-null, contains cache-related configuration.
57+
* @param source A string describing the source of the configuration of this library. Typically the class name of a {@link LibraryResolver}, sometimes with additional data, but see also {@link LibraryStep}.
58+
*/
59+
LibraryRecord(String name, String version, boolean trusted, boolean changelog, LibraryCachingConfiguration cachingConfiguration, String source) {
4760
this.name = name;
4861
this.version = version;
4962
this.trusted = trusted;
5063
this.changelog = changelog;
5164
this.cachingConfiguration = cachingConfiguration;
65+
this.directoryName = directoryNameFor(name, version, String.valueOf(trusted), source);
5266
}
5367

5468
@Exported
5569
public String getName() {
5670
return name;
5771
}
5872

73+
/**
74+
* Returns a partially unique name that can be safely used as a directory name.
75+
*
76+
* Uniqueness is based on the library name, version, whether it is trusted, and the source of the library.
77+
* {@link LibraryRetriever}-specific information such as the SCM is not used to produce this name.
78+
*/
79+
public String getDirectoryName() {
80+
return directoryName;
81+
}
82+
5983
@Exported
6084
public String getVersion() {
6185
return version;
@@ -78,7 +102,24 @@ public boolean isChangelog() {
78102

79103
@Override public String toString() {
80104
String cachingConfigurationStr = cachingConfiguration != null ? cachingConfiguration.toString() : "null";
81-
return "LibraryRecord{name=" + name + ", version=" + version + ", variables=" + variables + ", trusted=" + trusted + ", changelog=" + changelog + ", cachingConfiguration=" + cachingConfigurationStr + '}';
105+
return "LibraryRecord{name=" + name + ", version=" + version + ", variables=" + variables + ", trusted=" + trusted + ", changelog=" + changelog + ", cachingConfiguration=" + cachingConfigurationStr + ", directoryName=" + directoryName + '}';
106+
}
107+
108+
private Object readResolve() {
109+
if (directoryName == null) {
110+
// Builds started before directoryName was added must continue to use the library name as the directory name.
111+
directoryName = name;
112+
}
113+
return this;
114+
}
115+
116+
public static String directoryNameFor(String... data) {
117+
for (String datum : data) {
118+
if (datum.contains(ASCII_UNIT_SEPARATOR)) { // Very unlikely to appear in legitimate user-controlled text.
119+
throw new IllegalStateException("Unable to create directory name due to control character in " + datum);
120+
}
121+
}
122+
return DIRECTORY_NAME_KEY.mac(String.join(ASCII_UNIT_SEPARATOR, data));
82123
}
83124

84125
}

src/main/java/org/jenkinsci/plugins/workflow/libs/LibraryResolver.java

+27
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,31 @@ public abstract class LibraryResolver implements ExtensionPoint {
8585
return Collections.emptySet();
8686
}
8787

88+
/**
89+
* Used by some implementations of {@link LibraryResolver} to prevent libraries with the same name that are
90+
* configured in distinct trust domains from using the same cache directory.
91+
*
92+
* For example, {@link FolderLibraries.ForJob} may return libraries from different folders, and a user who is able
93+
* to configure a library in one folder may not be able to configure a library in another folder. To prevent this
94+
* from causing issues, {@link FolderLibraries.ForJob#forGroup} returns instances of this class where {@code source}
95+
* includes the full name of the folder where the library is configured.
96+
*/
97+
static class ResolvedLibraryConfiguration extends LibraryConfiguration {
98+
private final String source;
99+
100+
ResolvedLibraryConfiguration(LibraryConfiguration config, @NonNull String source) {
101+
super(config.getName(), config.getRetriever());
102+
setDefaultVersion(config.getDefaultVersion());
103+
setImplicit(config.isImplicit());
104+
setAllowVersionOverride(config.isAllowVersionOverride());
105+
setIncludeInChangesets(config.getIncludeInChangesets());
106+
setCachingConfiguration(config.getCachingConfiguration());
107+
this.source = source;
108+
}
109+
110+
@NonNull String getSource() {
111+
return source;
112+
}
113+
}
114+
88115
}

src/main/java/org/jenkinsci/plugins/workflow/libs/LibraryStep.java

+12-7
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ public static class Execution extends AbstractSynchronousNonBlockingStepExecutio
166166
boolean trusted = false;
167167
Boolean changelog = step.getChangelog();
168168
LibraryCachingConfiguration cachingConfiguration = null;
169+
// Note that cachingConfiguration is only ever non-null if source is overwritten below, so the default value of source will never be used when caching is enabled.
170+
String source = LibraryStep.class.getName() + " " + run.getExternalizableId();
169171
LibraryRetriever retriever = step.getRetriever();
170172
if (retriever == null) {
171173
for (LibraryResolver resolver : ExtensionList.lookup(LibraryResolver.class)) {
@@ -176,6 +178,10 @@ public static class Execution extends AbstractSynchronousNonBlockingStepExecutio
176178
version = cfg.defaultedVersion(version);
177179
changelog = cfg.defaultedChangelogs(changelog);
178180
cachingConfiguration = cfg.getCachingConfiguration();
181+
source = resolver.getClass().getName();
182+
if (cfg instanceof LibraryResolver.ResolvedLibraryConfiguration) {
183+
source = ((LibraryResolver.ResolvedLibraryConfiguration) cfg).getSource();
184+
}
179185
break;
180186
}
181187
}
@@ -186,8 +192,7 @@ public static class Execution extends AbstractSynchronousNonBlockingStepExecutio
186192
} else if (version == null) {
187193
throw new AbortException("Must specify a version for library " + name);
188194
}
189-
190-
LibraryRecord record = new LibraryRecord(name, version, trusted, changelog, cachingConfiguration);
195+
LibraryRecord record = new LibraryRecord(name, version, trusted, changelog, cachingConfiguration, source);
191196
LibrariesAction action = run.getAction(LibrariesAction.class);
192197
if (action == null) {
193198
action = new LibrariesAction(Lists.newArrayList(record));
@@ -197,7 +202,7 @@ public static class Execution extends AbstractSynchronousNonBlockingStepExecutio
197202
for (LibraryRecord existing : libraries) {
198203
if (existing.name.equals(name)) {
199204
listener.getLogger().println("Only using first definition of library " + name);
200-
return new LoadedClasses(name, trusted, changelog, run);
205+
return new LoadedClasses(name, existing.getDirectoryName(), trusted, changelog, run);
201206
}
202207
}
203208
List<LibraryRecord> newLibraries = new ArrayList<>(libraries);
@@ -207,11 +212,11 @@ public static class Execution extends AbstractSynchronousNonBlockingStepExecutio
207212
listener.getLogger().println("Loading library " + record.name + "@" + record.version);
208213
CpsFlowExecution exec = (CpsFlowExecution) getContext().get(FlowExecution.class);
209214
GroovyClassLoader loader = (trusted ? exec.getTrustedShell() : exec.getShell()).getClassLoader();
210-
for (URL u : LibraryAdder.retrieve(record.name, record.version, retriever, record.trusted, record.changelog, record.cachingConfiguration, listener, run, (CpsFlowExecution) getContext().get(FlowExecution.class), record.variables)) {
215+
for (URL u : LibraryAdder.retrieve(record, retriever, listener, run, (CpsFlowExecution) getContext().get(FlowExecution.class))) {
211216
loader.addURL(u);
212217
}
213218
run.save(); // persist changes to LibrariesAction.libraries*.variables
214-
return new LoadedClasses(name, trusted, changelog, run);
219+
return new LoadedClasses(name, record.getDirectoryName(), trusted, changelog, run);
215220
}
216221

217222
}
@@ -228,8 +233,8 @@ public static final class LoadedClasses extends GroovyObjectSupport implements S
228233
/** {@code file:/…/libs/NAME/src/} */
229234
private final @NonNull String srcUrl;
230235

231-
LoadedClasses(String library, boolean trusted, Boolean changelog, Run<?,?> run) {
232-
this(library, trusted, changelog, "", null, /* cf. LibraryAdder.retrieve */ new File(run.getRootDir(), "libs/" + library + "/src").toURI().toString());
236+
LoadedClasses(String library, String libraryDirectoryName, boolean trusted, Boolean changelog, Run<?,?> run) {
237+
this(library, trusted, changelog, "", null, /* cf. LibraryAdder.retrieve */ new File(run.getRootDir(), "libs/" + libraryDirectoryName + "/src").toURI().toString());
233238
}
234239

235240
LoadedClasses(String library, boolean trusted, Boolean changelog, String prefix, String clazz, String srcUrl) {

src/main/java/org/jenkinsci/plugins/workflow/libs/SCMSourceRetriever.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,8 @@ static void doRetrieve(String name, boolean changelog, @NonNull SCM scm, String
185185
if (baseWorkspace == null) {
186186
throw new IOException(node.getDisplayName() + " may be offline");
187187
}
188-
dir = baseWorkspace.withSuffix(getFilePathSuffix() + "libs").child(name);
188+
String checkoutDirName = LibraryRecord.directoryNameFor(scm.getKey());
189+
dir = baseWorkspace.withSuffix(getFilePathSuffix() + "libs").child(checkoutDirName);
189190
} else { // should not happen, but just in case:
190191
throw new AbortException("Cannot check out in non-top-level build");
191192
}
@@ -194,6 +195,8 @@ static void doRetrieve(String name, boolean changelog, @NonNull SCM scm, String
194195
throw new IOException(node.getDisplayName() + " may be offline");
195196
}
196197
try (WorkspaceList.Lease lease = computer.getWorkspaceList().allocate(dir)) {
198+
// Write the SCM key to a file as a debugging aid.
199+
lease.path.withSuffix("-scm-key.txt").write(scm.getKey(), "UTF-8");
197200
retrySCMOperation(listener, () -> {
198201
delegate.checkout(run, lease.path, listener, node.createLauncher(listener));
199202
return null;

0 commit comments

Comments
 (0)