Skip to content

Commit 0e0ac58

Browse files
committed
Resolve #469 : Add -C,--canonicalRoot to whitelist symlink target roots
1 parent fbe755c commit 0e0ac58

File tree

6 files changed

+98
-30
lines changed

6 files changed

+98
-30
lines changed

opengrok-indexer/src/main/java/org/opengrok/indexer/configuration/Configuration.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
/*
2121
* Copyright (c) 2007, 2018, Oracle and/or its affiliates. All rights reserved.
22-
* Portions Copyright (c) 2017-2018, Chris Fraire <[email protected]>.
22+
* Portions Copyright (c) 2017-2019, Chris Fraire <[email protected]>.
2323
*/
2424
package org.opengrok.indexer.configuration;
2525

@@ -201,6 +201,7 @@ public final class Configuration {
201201
private String CTagsExtraOptionsFile;
202202
private int scanningDepth;
203203
private Set<String> allowedSymlinks;
204+
private Set<String> canonicalRoots;
204205
private boolean obfuscatingEMailAddresses;
205206
private boolean chattyStatusPage;
206207
private final Map<String, String> cmds; // repository type -> command
@@ -441,6 +442,7 @@ public Configuration() {
441442
//setBugPage("http://bugs.myserver.org/bugdatabase/view_bug.do?bug_id=");
442443
setBugPattern("\\b([12456789][0-9]{6})\\b");
443444
setCachePages(5);
445+
setCanonicalRoots(new HashSet<>());
444446
setCommandTimeout(600); // 10 minutes
445447
setInteractiveCommandTimeout(30);
446448
setCompressXref(true);
@@ -1173,6 +1175,14 @@ public void setAllowedSymlinks(Set<String> allowedSymlinks) {
11731175
this.allowedSymlinks = allowedSymlinks;
11741176
}
11751177

1178+
public Set<String> getCanonicalRoots() {
1179+
return canonicalRoots;
1180+
}
1181+
1182+
public void setCanonicalRoots(Set<String> canonicalRoots) {
1183+
this.canonicalRoots = canonicalRoots;
1184+
}
1185+
11761186
public boolean isObfuscatingEMailAddresses() {
11771187
return obfuscatingEMailAddresses;
11781188
}

opengrok-indexer/src/main/java/org/opengrok/indexer/configuration/RuntimeEnvironment.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ public String getPathRelativeToSourceRoot(File file)
431431
}
432432

433433
String maybeRelPath = PathUtils.getRelativeToCanonical(file.getPath(),
434-
sourceRoot, getAllowedSymlinks());
434+
sourceRoot, getAllowedSymlinks(), getCanonicalRoots());
435435
File maybeRelFile = new File(maybeRelPath);
436436
if (!maybeRelFile.isAbsolute()) {
437437
/*
@@ -1209,6 +1209,15 @@ public void setAllowedSymlinks(Set<String> allowedSymlinks) {
12091209
setConfigurationValue("allowedSymlinks", allowedSymlinks);
12101210
}
12111211

1212+
@SuppressWarnings("unchecked")
1213+
public Set<String> getCanonicalRoots() {
1214+
return (Set<String>) getConfigurationValue("canonicalRoots");
1215+
}
1216+
1217+
public void setCanonicalRoots(Set<String> canonicalRoots) {
1218+
setConfigurationValue("canonicalRoots", canonicalRoots);
1219+
}
1220+
12121221
/**
12131222
* Return whether e-mail addresses should be obfuscated in the xref.
12141223
* @return if we obfuscate emails

opengrok-indexer/src/main/java/org/opengrok/indexer/index/IndexDatabase.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1000,7 +1000,21 @@ private boolean acceptSymlink(Path absolute, File canonical, AcceptSymlinkRet re
10001000
}
10011001
}
10021002

1003-
Set<String> allowedSymlinks = RuntimeEnvironment.getInstance().getAllowedSymlinks();
1003+
RuntimeEnvironment env = RuntimeEnvironment.getInstance();
1004+
1005+
Set<String> canonicalRoots = env.getCanonicalRoots();
1006+
for (String canonicalRoot : canonicalRoots) {
1007+
if (canonical1.startsWith(canonicalRoot)) {
1008+
if (LOGGER.isLoggable(Level.FINE)) {
1009+
LOGGER.log(Level.FINE, "Allowed symlink {0} per canonical root {1}",
1010+
new Object[] {absolute1, canonical1});
1011+
}
1012+
acceptedNonlocalSymlinks.put(canonical1, absolute1);
1013+
return true;
1014+
}
1015+
}
1016+
1017+
Set<String> allowedSymlinks = env.getAllowedSymlinks();
10041018
for (String allowedSymlink : allowedSymlinks) {
10051019
String allowedTarget;
10061020
try {

opengrok-indexer/src/main/java/org/opengrok/indexer/index/Indexer.java

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ public final class Indexer {
116116

117117
private static final Set<String> repositories = new HashSet<>();
118118
private static final HashSet<String> allowedSymlinks = new HashSet<>();
119+
private static final HashSet<String> canonicalRoots = new HashSet<>();
119120
private static final Set<String> defaultProjects = new TreeSet<>();
120121
private static RuntimeEnvironment env = null;
121122
private static String webappURI = null;
@@ -205,6 +206,9 @@ public static void main(String[] argv) {
205206
allowedSymlinks.addAll(cfg.getAllowedSymlinks());
206207
cfg.setAllowedSymlinks(allowedSymlinks);
207208

209+
canonicalRoots.addAll(cfg.getCanonicalRoots());
210+
cfg.setCanonicalRoots(canonicalRoots);
211+
208212
// Assemble the unprocessed command line arguments (possibly
209213
// a list of paths). This will be used to perform more fine
210214
// grained checking in invalidateRepositories().
@@ -473,6 +477,22 @@ public static String[] parseOptions(String[] argv) throws ParseException {
473477
}
474478
);
475479

480+
parser.on("-C", "--canonicalRoot", "=/path/",
481+
"Allow symlinks to canonical targets starting with the specified",
482+
"root without otherwise needing to specify -N,--symlink for such",
483+
"symlinks. A canonical root must end with a file separator. For",
484+
"security a canonical root cannot be the root directory.",
485+
"Option may be repeated.").Do(v -> {
486+
String root = (String) v;
487+
if (!root.endsWith("/") && !root.endsWith("\\")) {
488+
die("--canonicalRoot must end with a separator");
489+
}
490+
if (root.equals("/") || root.equals("\\")) {
491+
die("--canonicalRoot cannot be the root directory");
492+
}
493+
canonicalRoots.add(root);
494+
});
495+
476496
parser.on("-c", "--ctags", "=/path/to/ctags",
477497
"Path to Universal Ctags",
478498
"By default takes the Universal Ctags in PATH.").
@@ -580,11 +600,13 @@ public static String[] parseOptions(String[] argv) throws ParseException {
580600
parser.on("-N", "--symlink", "=/path/to/symlink",
581601
"Allow this symlink to be followed. Option may be repeated.",
582602
"By default only symlinks directly under source root directory",
583-
"are allowed.").Do(symlink -> allowedSymlinks.add((String) symlink));
603+
"are allowed. See also -C,--canonicalRoot").Do(v ->
604+
allowedSymlinks.add((String) v));
584605

585606
parser.on("-n", "--noIndex",
586-
"Do not generate indexes and other data (such as history cache and xref files), " +
587-
"but process all other command line options.").Do(v -> runIndex = false);
607+
"Do not generate indexes and other data (such as history cache",
608+
"and xref files), but process all other command line options.").Do(v ->
609+
runIndex = false);
588610

589611
parser.on("-O", "--optimize", "=on|off", ON_OFF, Boolean.class,
590612
"Turn on/off the optimization of the index database",
@@ -621,8 +643,8 @@ public static String[] parseOptions(String[] argv) throws ParseException {
621643
parser.on("-p", "--defaultProject", "=/path/to/default/project",
622644
"This is the path to the project that should be selected",
623645
"by default in the web application (when no other project",
624-
"set either in cookie or in parameter). May be used multiple",
625-
"times for several projects. Use \"__all__\" for all projects.",
646+
"set either in cookie or in parameter). Option may be repeated",
647+
"to specify several projects. Use \"__all__\" for all projects.",
626648
"You should strip off the source root.").Do(v -> {
627649
defaultProjects.add((String) v);
628650
});
@@ -651,10 +673,9 @@ public static String[] parseOptions(String[] argv) throws ParseException {
651673
});
652674

653675
parser.on("--repository", "=repository",
654-
"Generate history for specific repository specified as relative path to source root. ",
655-
"Can be used multiple times. Assumes history is on.").Do(repo -> {
656-
repositories.add((String) repo);
657-
});
676+
"Generate history for specific repository specified as relative",
677+
"path to source root. Assumes -H,--history is on. Option may be",
678+
"repeated.").Do(v -> repositories.add((String) v));
658679

659680
parser.on("-R /path/to/configuration",
660681
"Read configuration from the specified file.").Do(v -> {

opengrok-indexer/src/main/java/org/opengrok/indexer/util/PathUtils.java

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,23 +44,20 @@ public class PathUtils {
4444
LoggerFactory.getLogger(PathUtils.class);
4545

4646
/**
47-
* Calls
48-
* {@link #getRelativeToCanonical(java.lang.String, java.lang.String, java.util.Set)}
49-
* with {@code path}, {@code canonical}, and {@code allowedSymlinks=null}
50-
* (to disable validation of links).
47+
* Calls {@link #getRelativeToCanonical(String, String, Set, Set)}
48+
* with {@code path}, {@code canonical}, {@code allowedSymlinks=null}, and
49+
* {@code canonicalRoots=null} (to disable validation of links).
5150
* @param path a non-canonical (or canonical) path to compare
5251
* @param canonical a canonical path to compare against
53-
* @return a relative path determined as described for
54-
* {@link #getRelativeToCanonical(java.lang.String, java.lang.String, java.util.Set)}
55-
* when {@code allowedSymlinks==null} -- or {@code path} if no canonical
56-
* relativity is found.
52+
* @return a relative path determined as described -- or {@code path} if no
53+
* canonical relativity is found.
5754
* @throws IOException if an error occurs determining canonical paths
5855
* for portions of {@code path}
5956
*/
6057
public static String getRelativeToCanonical(String path, String canonical)
6158
throws IOException {
6259
try {
63-
return getRelativeToCanonical(path, canonical, null);
60+
return getRelativeToCanonical(path, canonical, null, null);
6461
} catch (ForbiddenSymlinkException e) {
6562
// should not get here with allowedSymlinks==null
6663
return path;
@@ -88,8 +85,12 @@ public static String getRelativeToCanonical(String path, String canonical)
8885
* @param path a non-canonical (or canonical) path to compare
8986
* @param canonical a canonical path to compare against
9087
* @param allowedSymlinks optional set of allowed symbolic links, so that
91-
* any links encountered within {@code path} and not covered by the set
92-
* will abort the algorithm
88+
* any links encountered within {@code path} and not covered by the set (or
89+
* whitelisted in a defined {@code canonicalRoots}) will abort the algorithm
90+
* @param canonicalRoots optional set of allowed canonicalRoots, so that
91+
* any checks done because of a defined {@code allowedSymlinks} will first
92+
* check against the whitelist of canonical roots and possibly short-circuit
93+
* the explicit validation against {@code allowedSymlinks}.
9394
* @return a relative path determined as described above -- or {@code path}
9495
* if no canonical relativity is found
9596
* @throws IOException if an error occurs determining canonical paths
@@ -99,7 +100,7 @@ public static String getRelativeToCanonical(String path, String canonical)
99100
* @throws InvalidPathException if path cannot be decoded
100101
*/
101102
public static String getRelativeToCanonical(String path, String canonical,
102-
Set<String> allowedSymlinks)
103+
Set<String> allowedSymlinks, Set<String> canonicalRoots)
103104
throws IOException, ForbiddenSymlinkException, InvalidPathException {
104105

105106
if (path.equals(canonical)) {
@@ -127,7 +128,8 @@ public static String getRelativeToCanonical(String path, String canonical,
127128
if (allowedSymlinks != null) {
128129
String iterOriginal = iterPath.getPath();
129130
if (Files.isSymbolicLink(Paths.get(iterOriginal)) &&
130-
!isAllowedSymlink(iterCanon, allowedSymlinks)) {
131+
!isWhitelisted(iterCanon, canonicalRoots) &&
132+
!isAllowedSymlink(iterCanon, allowedSymlinks)) {
131133
String format = String.format("%1$s is prohibited symlink",
132134
iterOriginal);
133135
LOGGER.finest(format);
@@ -181,6 +183,17 @@ private static boolean isAllowedSymlink(String canonicalFile,
181183
return false;
182184
}
183185

186+
private static boolean isWhitelisted(String canonical, Set<String> canonicalRoots) {
187+
if (canonicalRoots != null) {
188+
for (String canonicalRoot : canonicalRoots) {
189+
if (canonical.startsWith(canonicalRoot)) {
190+
return true;
191+
}
192+
}
193+
}
194+
return false;
195+
}
196+
184197
/** Private to enforce static. */
185198
private PathUtils() {
186199
}

opengrok-indexer/src/test/java/org/opengrok/indexer/util/PathUtilsTest.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@
1818
*/
1919

2020
/*
21-
* Copyright (c) 2017, Chris Fraire <[email protected]>.
21+
* Copyright (c) 2017, 2019, Chris Fraire <[email protected]>.
2222
* Copyright (c) 2008, 2018, Oracle and/or its affiliates. All rights reserved.
2323
*/
2424
package org.opengrok.indexer.util;
2525

26+
import static org.junit.Assert.assertEquals;
27+
import static org.junit.Assert.assertTrue;
28+
2629
import java.io.File;
2730
import java.io.IOException;
2831
import java.net.URI;
@@ -35,8 +38,6 @@
3538
import java.util.Set;
3639
import org.junit.After;
3740
import org.junit.Assert;
38-
import static org.junit.Assert.assertEquals;
39-
import static org.junit.Assert.assertTrue;
4041
import org.junit.Ignore;
4142
import org.junit.Rule;
4243
import org.junit.Test;
@@ -131,7 +132,7 @@ public void shouldHandleLinksOfArbitraryDepthWithValidation()
131132
ForbiddenSymlinkException expex = null;
132133
try {
133134
PathUtils.getRelativeToCanonical(sympath.toString(), realDir1Canon,
134-
allowedSymLinks);
135+
allowedSymLinks, null);
135136
} catch (ForbiddenSymlinkException e) {
136137
expex = e;
137138
}
@@ -141,7 +142,7 @@ public void shouldHandleLinksOfArbitraryDepthWithValidation()
141142
// Test v. realDir1 canonical with validation and an allowed link
142143
allowedSymLinks.add(symlink2.getPath());
143144
rel = PathUtils.getRelativeToCanonical(sympath.toString(),
144-
realDir1Canon, allowedSymLinks);
145+
realDir1Canon, allowedSymLinks, null);
145146
assertEquals("because link is OKed", "b/" + SYMLINK2, rel);
146147
}
147148

0 commit comments

Comments
 (0)