Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,21 @@ public BasicFolderResourceImpl(String path) {
this.isAutoPub=dotDavHelper.isAutoPub(path);
}

public Resource createNew(String newName, final InputStream in, final Long length, final String contentType) throws IOException, DotRuntimeException {
/**
* Creates a file named {@code newName} in this folder. A temp-style name goes to temp storage
* and must be a single plain path segment; any other name is stored as content.
*
* @param newName the file name received from the WebDAV client
* @param in the file content
* @param length the content length, if known
* @param contentType the content type, if known
* @return the created resource
* @throws IOException if the content cannot be read or written
* @throws DotRuntimeException if the file cannot be created
* @throws BadRequestException (HTTP 400) when a temp-style name is not a single plain path segment
*/
public Resource createNew(String newName, final InputStream in, final Long length, final String contentType)
throws IOException, DotRuntimeException, BadRequestException {

if(newName.matches("^\\.(.*)-Spotlight$")){
// http://jira.dotmarketing.net/browse/DOTCMS-7285
Expand Down Expand Up @@ -83,6 +97,7 @@ public Resource createNew(String newName, final InputStream in, final Long lengt
}
} else {

DotWebdavHelper.requirePlainSegment(this, newName);
return this.createNewTemporalResource(newName, in);
}
} // createNew.
Expand Down
204 changes: 197 additions & 7 deletions dotCMS/src/main/java/com/dotmarketing/webdav/DotWebdavHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import com.bradmcevoy.http.Request;
import com.bradmcevoy.http.Request.Method;
import com.bradmcevoy.http.Resource;
import com.bradmcevoy.http.exceptions.BadRequestException;
import com.dotmarketing.beans.Host;
import com.dotmarketing.beans.Identifier;
import com.dotmarketing.beans.Permission;
Expand Down Expand Up @@ -73,6 +74,7 @@
import java.util.List;
import java.util.Optional;
import java.util.Timer;
import java.util.function.Supplier;
import org.apache.commons.lang.StringUtils;
import org.apache.oro.text.regex.MalformedPatternException;
import org.apache.oro.text.regex.Perl5Compiler;
Expand Down Expand Up @@ -101,7 +103,8 @@ protected Perl5Matcher initialValue() {
}
};
private org.apache.oro.text.regex.Pattern tempResourcePattern;
private String tempFolderPath = "dotwebdav";
/** Sub-folder of the asset temp path that holds WebDAV temp storage, apart from other features. */
private static final String TEMP_FOLDER_NAME = "dotwebdav";

private HostAPI hostAPI = APILocator.getHostAPI();
private FolderAPI folderAPI = APILocator.getFolderAPI();
Expand Down Expand Up @@ -391,14 +394,26 @@ public Folder loadFolder(String url,User user) throws IOException{
return folder;
}

/**
* Resolves a temporary WebDAV resource from a request-derived path. The path is built under
* {@link #getTempDir()} and must resolve inside it; otherwise {@code null} is returned and the
* caller treats it as "not found".
*
* @param url the request URL (endpoint mapping is stripped internally)
* @return the temp {@link File}, or {@code null} when the path has a {@code .} or {@code ..}
* segment or is not inside the temp directory
*/
public File loadTempFile(String url){
try {
url = stripMapping(url);
} catch (IOException e) {
Logger.error( this, "Error happened with uri: [" + url + "]", e);
Logger.error(this, "Unable to strip the WebDAV mapping from a temp path", e);
}
try {
return resolveTempPath(url, () -> new IOException("Invalid WebDAV temp path"));
} catch (IOException e) {
return null;
}
Logger.debug(this, "Getting temp file from path " + url);
return new File(getTempDir().getPath() + url);
}

/**
Expand Down Expand Up @@ -503,8 +518,165 @@ public List<Resource> getChildrenOfFolder ( Folder parentFolder, User user, bool
return result;
}

/**
* Returns the directory that holds WebDAV temp storage: a dedicated sub-folder of the asset temp
* path, so WebDAV never shares a root with the other features that write there.
*
* @return the WebDAV temp directory
*/
public File getTempDir () {
return new File(ConfigUtils.getAssetTempPath());
return new File(ConfigUtils.getAssetTempPath(), TEMP_FOLDER_NAME);
}

/**
* Verifies that {@code candidate} resolves to a location strictly inside the WebDAV temp
* directory ({@link #getTempDir()}). The temp directory itself is not a valid temp resource.
* Canonicalization collapses {@code .} and {@code ..} segments and resolves symlinks, so the
* check holds however the path is written. Fails closed: any canonicalization error is treated
* as "not contained".
*
* @param candidate the {@link File} built from request-derived input
* @return {@code true} only when the canonical candidate is a descendant of the temp directory
*/
private boolean isWithinTempDir(final File candidate) {
try {
final String root = getTempDir().getCanonicalPath();
return candidate.getCanonicalPath().startsWith(root + File.separator);
} catch (IOException e) {
Logger.error(this, "WebDAV temp-path canonicalization failed; rejecting", e);
logRejection("Rejected a WebDAV temp path that could not be canonicalized");
return false;
}
}

/**
* Tells whether a path has a {@code .} or {@code ..} segment. Such segments are never needed to
* address temp storage, so a path that has one is rejected before it is resolved.
*
* @param path the request-derived path, using {@code /} or the platform separator
* @return {@code true} when any segment is exactly {@code .} or {@code ..}
*/
static boolean hasDotSegment(final String path) {
for (final String segment : path.split("[/\\\\]")) {
if (".".equals(segment) || "..".equals(segment)) {
return true;
}
}
return false;
}

/**
* Resolves a request-derived path under the temp directory. The path must have no {@code .} or
* {@code ..} segment and must resolve strictly inside the temp directory; otherwise the rejection
* is logged and the exception built by {@code rejection} is thrown.
*
* @param relativePath the path to append to the temp directory
* @param rejection builds the exception thrown when the path is not valid
* @param <E> the type of the rejection exception
* @return the resolved {@link File}
* @throws E when the path is not valid
*/
private <E extends Exception> File resolveTempPath(final String relativePath, final Supplier<E> rejection)
throws E {
if (hasDotSegment(relativePath)) {
logRejection("Rejected a WebDAV temp path with a dot segment");
throw rejection.get();
}
return requireWithinTempDir(new File(getTempDir().getPath() + relativePath), rejection);
}

/**
* Logs a rejected WebDAV temp-storage request to the security log, with the id of the user who
* made it. The message never includes request data.
*
* @param message what was rejected
*/
private static void logRejection(final String message) {
SecurityLogger.logWarn(DotWebdavHelper.class, () -> message + ", userId: " + currentUserId());
}

/**
* Returns the id of the user authenticated on the current WebDAV request, or {@code unknown} when
* the code does not run inside a WebDAV request.
*
* @return the current user id, or {@code unknown}
*/
private static String currentUserId() {
final Request request = HttpManager.request();
final Object tag = request != null && request.getAuthorization() != null
? request.getAuthorization().getTag() : null;
return tag instanceof User ? ((User) tag).getUserId() : "unknown";
}

/**
* Returns {@code candidate} when it resolves strictly inside the temp directory (see
* {@link #isWithinTempDir(File)}); otherwise logs the rejection and throws the exception built by
* {@code rejection}. Each caller supplies the exception that matches its own failure contract.
*
* @param candidate the {@link File} built from request-derived input
* @param rejection builds the exception thrown when the candidate is outside the temp directory
* @param <E> the type of the rejection exception
* @return {@code candidate}, unchanged
* @throws E when the candidate is not inside the temp directory
*/
private <E extends Exception> File requireWithinTempDir(final File candidate, final Supplier<E> rejection)
throws E {
if (!isWithinTempDir(candidate)) {
logRejection("Rejected a WebDAV temp path outside the temp directory");
throw rejection.get();
}
return candidate;
}

/**
* Tells whether a client-supplied name can be used as a child name in temp storage: a single
* plain path segment that is not empty, not {@code .} or {@code ..}, and contains no path
* separator or NUL character.
*
* @param name the name received from the WebDAV client
* @return {@code true} when the name is a single plain path segment
*/
static boolean isPlainSegment(final String name) {
return name != null
&& !name.isEmpty()
&& !".".equals(name)
&& !"..".equals(name)
&& name.indexOf('/') < 0
&& name.indexOf('\\') < 0
&& name.indexOf('\0') < 0;
}

/**
* Rejects a client-supplied name that is not a single plain path segment, before it reaches
* temp storage.
*
* @param resource the resource handling the request, reported in the rejection
* @param name the name received from the WebDAV client
* @throws BadRequestException (HTTP 400) when the name is not a single plain path segment
*/
static void requirePlainSegment(final Resource resource, final String name) throws BadRequestException {
if (!isPlainSegment(name)) {
logRejection("Rejected a WebDAV name that is not a single path segment");
throw new BadRequestException(resource, "Invalid name");
}
}

/**
* Resolves the target of a write into temp storage: {@code name} inside {@code folder}. The name
* must be a single plain path segment and the target must stay strictly inside the temp
* directory.
*
* @param resource the resource handling the request, reported in the rejection
* @param folder the temp folder receiving the write
* @param name the name received from the WebDAV client
* @return the target {@link File}
* @throws BadRequestException (HTTP 400) when the name or the target is not valid
*/
File resolveTempChild(final Resource resource, final File folder, final String name)
throws BadRequestException {
requirePlainSegment(resource, name);
return requireWithinTempDir(new File(folder, name),
() -> new BadRequestException(resource, "Invalid destination"));
}

public String getHostName ( String uri ) {
Expand All @@ -523,6 +695,14 @@ public boolean isTempResource(String path){
return false;
}

/**
* Creates a temporary WebDAV folder under {@link #getTempDir()}. The path must have no {@code .} or
* {@code ..} segment and resolve inside the temp directory; otherwise nothing is created.
*
* @param path the request-derived folder path (endpoint mapping is stripped internally)
* @return the created temp folder
* @throws DotRuntimeException if the path has a dot segment or is not inside the temp directory
*/
public File createTempFolder(String path){
try {
path = stripMapping(path);
Expand All @@ -535,7 +715,8 @@ public File createTempFolder(String path){
path = path.substring(1, path.length());
}
path = path.replace("/", File.separator);
File f = new File(getTempDir().getPath() + File.separator + path);
final File f = resolveTempPath(File.separator + path,
() -> new DotRuntimeException("Invalid WebDAV temp folder path"));
f.mkdirs();
return f;
}
Expand Down Expand Up @@ -575,8 +756,17 @@ public File copyFileToTemp(IFileAsset file, File tempFolder) throws IOException{
return nf;
}

/**
* Creates a temporary WebDAV file under {@link #getTempDir()}. The path must have no {@code .} or
* {@code ..} segment and resolve inside the temp directory; otherwise nothing is created.
*
* @param path the request-derived file path, relative to the temp directory
* @return the created temp file
* @throws IOException if the path has a dot segment or is not inside the temp directory, or the
* file cannot be created
*/
public File createTempFile(String path) throws IOException{
File file = new File(getTempDir().getPath() + path);
final File file = resolveTempPath(path, () -> new IOException("Invalid WebDAV temp file path"));
String p = file.getPath().substring(0,file.getPath().lastIndexOf(File.separator));
File f = new File(p);
f.mkdirs();
Expand Down
Loading
Loading