org.owasp.esapi.reference.DefaultExecutor Maven / Gradle / Ivy
/**
* OWASP Enterprise Security API (ESAPI)
*
* This file is part of the Open Web Application Security Project (OWASP)
* Enterprise Security API (ESAPI) project. For details, please see
* http://www.owasp.org/index.php/ESAPI.
*
* Copyright (c) 2007 - The OWASP Foundation
*
* The ESAPI is published by OWASP under the BSD license. You should read and accept the
* LICENSE before you use, modify, and/or redistribute this software.
*
* @author Jeff Williams Aspect Security
* @created 2007
*/
package org.owasp.esapi.reference;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
import java.util.Map;
import org.owasp.esapi.ESAPI;
import org.owasp.esapi.ExecuteResult;
import org.owasp.esapi.Executor;
import org.owasp.esapi.Logger;
import org.owasp.esapi.codecs.Codec;
import org.owasp.esapi.codecs.UnixCodec;
import org.owasp.esapi.codecs.WindowsCodec;
import org.owasp.esapi.errors.ExecutorException;
/**
* Reference implementation of the Executor interface. This implementation is very restrictive. Commands must exactly
* equal the canonical path to an executable on the system.
*
* Valid characters for parameters are codec dependent, but will usually only include alphanumeric, forward-slash, and dash.
*
* @author Jeff Williams (jeff.williams .at. aspectsecurity.com) Aspect Security
* @since June 1, 2007
* @see org.owasp.esapi.Executor
*/
public class DefaultExecutor implements org.owasp.esapi.Executor {
private static volatile Executor singletonInstance;
public static Executor getInstance() {
if ( singletonInstance == null ) {
synchronized ( DefaultExecutor.class ) {
if ( singletonInstance == null ) {
singletonInstance = new DefaultExecutor();
}
}
}
return singletonInstance;
}
/** The logger. */
private final Logger logger = ESAPI.getLogger("Executor");
private Codec codec = null;
//private final int MAX_SYSTEM_COMMAND_LENGTH = 2500;
/**
* Instantiate a new Executor
*/
private DefaultExecutor() {
if ( System.getProperty("os.name").indexOf("Windows") != -1 ) {
logger.warning( Logger.SECURITY_SUCCESS, "Using WindowsCodec for Executor. If this is not running on Windows this could allow injection" );
codec = new WindowsCodec();
} else {
logger.warning( Logger.SECURITY_SUCCESS, "Using UnixCodec for Executor. If this is not running on Unix this could allow injection" );
codec = new UnixCodec();
}
}
/**
* {@inheritDoc}
*/
public ExecuteResult executeSystemCommand(File executable, List params) throws ExecutorException {
File workdir = ESAPI.securityConfiguration().getWorkingDirectory();
boolean logParams = false;
boolean redirectErrorStream = false;
return executeSystemCommand( executable, params, workdir, codec, logParams, redirectErrorStream );
}
/**
* {@inheritDoc}
*
* The reference implementation sets the work directory, escapes the parameters as per the Codec in use,
* and then executes the command without using concatenation. The exact, absolute, canonical path of each
* executable must be listed as an approved executable in the ESAPI properties. The executable must also
* exist on the disk. All failures will be logged, along with parameters if specified. Set the logParams to false if
* you are going to invoke this interface with confidential information.
*/
public ExecuteResult executeSystemCommand(File executable, List params, File workdir, Codec codec, boolean logParams, boolean redirectErrorStream ) throws ExecutorException {
try {
// executable must exist
if (!executable.exists()) {
throw new ExecutorException("Execution failure", "No such executable: " + executable);
}
// executable must use canonical path
if ( !executable.isAbsolute() ) {
throw new ExecutorException("Execution failure", "Attempt to invoke an executable using a non-absolute path: " + executable);
}
// executable must use canonical path
if ( !executable.getPath().equals( executable.getCanonicalPath() ) ) {
throw new ExecutorException("Execution failure", "Attempt to invoke an executable using a non-canonical path: " + executable);
}
// exact, absolute, canonical path to executable must be listed in ESAPI configuration
List approved = ESAPI.securityConfiguration().getAllowedExecutables();
if (!approved.contains(executable.getPath())) {
throw new ExecutorException("Execution failure", "Attempt to invoke executable that is not listed as an approved executable in ESAPI configuration: " + executable.getPath() + " not listed in " + approved );
}
// escape any special characters in the parameters
for ( int i = 0; i < params.size(); i++ ) {
String param = (String)params.get(i);
params.set( i, ESAPI.encoder().encodeForOS(codec, param));
}
// working directory must exist
if (!workdir.exists()) {
throw new ExecutorException("Execution failure", "No such working directory for running executable: " + workdir.getPath());
}
// set the command into the list and create command array
params.add(0, executable.getCanonicalPath());
// Legacy - this is how to implement in Java 1.4
// String[] command = (String[])params.toArray( new String[0] );
// Process process = Runtime.getRuntime().exec(command, new String[0], workdir);
// The following is host to implement in Java 1.5+
ProcessBuilder pb = new ProcessBuilder(params);
Map env = pb.environment();
env.clear(); // Security check - clear environment variables!
pb.directory(workdir);
pb.redirectErrorStream(redirectErrorStream);
if ( logParams ) {
logger.debug(Logger.SECURITY_SUCCESS, "Initiating executable: " + executable + " " + params + " in " + workdir);
} else {
logger.debug(Logger.SECURITY_SUCCESS, "Initiating executable: " + executable + " [sensitive parameters obscured] in " + workdir);
}
final StringBuilder outputBuffer = new StringBuilder();
final StringBuilder errorsBuffer = new StringBuilder();
final Process process = pb.start();
try {
ReadThread errorReader;
if (!redirectErrorStream) {
errorReader = new ReadThread(process.getErrorStream(), errorsBuffer);
errorReader.start();
} else {
errorReader = null;
}
readStream( process.getInputStream(), outputBuffer );
if (errorReader != null) {
errorReader.join();
if (errorReader.exception != null) {
throw errorReader.exception;
}
}
process.waitFor();
} catch (Throwable e) {
process.destroy();
throw new ExecutorException("Execution failure", "Exception thrown during execution of system command: " + e.getMessage(), e);
}
String output = outputBuffer.toString();
String errors = errorsBuffer.toString();
int exitValue = process.exitValue();
if ( errors != null && errors.length() > 0 ) {
String logErrors = errors;
final int MAX_LEN = 256;
if (logErrors.length() > MAX_LEN) {
logErrors = logErrors.substring(0, MAX_LEN) + "(truncated at "+MAX_LEN+" characters)";
}
logger.warning( Logger.SECURITY_SUCCESS, "Error during system command: " + logErrors );
}
if ( exitValue != 0 ) {
logger.warning( Logger.EVENT_FAILURE, "System command exited with non-zero status: " + exitValue );
}
logger.debug(Logger.SECURITY_SUCCESS, "System command complete");
return new ExecuteResult(exitValue, output, errors);
} catch (IOException e) {
throw new ExecutorException("Execution failure", "Exception thrown during execution of system command: " + e.getMessage(), e);
}
}
/**
* readStream reads lines from an input stream and returns all of them in a single string
*
* @param is
* input stream to read from
* @throws IOException
*/
private static void readStream( InputStream is, StringBuilder sb ) throws IOException {
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append('\n');
}
}
private static class ReadThread extends Thread {
volatile IOException exception;
private final InputStream stream;
private final StringBuilder buffer;
ReadThread(InputStream stream, StringBuilder buffer) {
this.stream = stream;
this.buffer = buffer;
}
@Override
public void run() {
try {
readStream(stream, buffer);
} catch (IOException e) {
exception = e;
}
}
}
}