server,utils: improve js interpretation functionality

Make JS interpretation functionalities configurable via a hidden config
- js.interpretation.enabled
Default value is false, making such functionalities disabled, ie, new
heuristic rules cannot be added or updated.

For JsInterpretor, use --no-java --no-syntax-extensions args and a deny-all ClassFilter.
Replace string-spliced vars with ENGINE_SCOPE Bindings, use a fresh ScriptContext per run, and compile before eval.
Use a named daemon worker with hard timeouts and capture stdout.

Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
This commit is contained in:
Abhishek Kumar 2025-09-17 01:20:38 +05:30 committed by Wei Zhou
parent c8d44d92a7
commit 03a4b9f4fd
No known key found for this signature in database
GPG Key ID: 1503DFE7C8226103
12 changed files with 253 additions and 108 deletions

View File

@ -20,7 +20,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.cloud.user.UserData;
import org.apache.cloudstack.api.command.admin.cluster.ListClustersCmd;
import org.apache.cloudstack.api.command.admin.config.ListCfgGroupsByCmd;
import org.apache.cloudstack.api.command.admin.config.ListCfgsByCmd;
@ -66,6 +65,7 @@ import org.apache.cloudstack.api.command.user.vm.GetVMPasswordCmd;
import org.apache.cloudstack.api.command.user.vmgroup.UpdateVMGroupCmd;
import org.apache.cloudstack.config.Configuration;
import org.apache.cloudstack.config.ConfigurationGroup;
import org.apache.cloudstack.framework.config.ConfigKey;
import com.cloud.alert.Alert;
import com.cloud.capacity.Capacity;
@ -85,6 +85,7 @@ import com.cloud.storage.GuestOSHypervisor;
import com.cloud.storage.GuestOsCategory;
import com.cloud.storage.StoragePool;
import com.cloud.user.SSHKeyPair;
import com.cloud.user.UserData;
import com.cloud.utils.Pair;
import com.cloud.utils.Ternary;
import com.cloud.vm.InstanceGroup;
@ -98,6 +99,14 @@ import com.cloud.vm.VirtualMachine.Type;
public interface ManagementService {
static final String Name = "management-server";
ConfigKey<Boolean> JsInterpretationEnabled = new ConfigKey<>("Hidden"
, Boolean.class
, "js.interpretation.enabled"
, "false"
, "Enable/Disable all JavaScript interpretation related functionalities to create or update Javascript rules."
, false
, ConfigKey.Scope.Global);
/**
* returns the a map of the names/values in the configuration table
*
@ -481,4 +490,6 @@ public interface ManagementService {
Pair<Boolean, String> patchSystemVM(PatchSystemVMCmd cmd);
void checkJsInterpretationAllowedIfNeededForParameterValue(String paramName, boolean paramValue);
}

View File

@ -32,7 +32,6 @@ import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
import com.cloud.user.Account;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.quota.activationrule.presetvariables.GenericPresetVariable;
import org.apache.cloudstack.quota.activationrule.presetvariables.PresetVariableHelper;
@ -62,6 +61,7 @@ import org.springframework.stereotype.Component;
import com.cloud.usage.UsageVO;
import com.cloud.usage.dao.UsageDao;
import com.cloud.user.Account;
import com.cloud.user.AccountVO;
import com.cloud.user.dao.AccountDao;
import com.cloud.utils.DateUtil;
@ -467,7 +467,7 @@ public class QuotaManagerImpl extends ManagerBase implements QuotaManager {
}
jsInterpreter.injectStringVariable("resourceType", presetVariables.getResourceType());
jsInterpreter.injectVariable("resourceType", presetVariables.getResourceType());
jsInterpreter.injectVariable("value", presetVariables.getValue().toString());
jsInterpreter.injectVariable("zone", presetVariables.getZone().toString());
}

View File

@ -270,7 +270,7 @@ public class QuotaManagerImplTest {
Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("account"), Mockito.anyString());
Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("domain"), Mockito.anyString());
Mockito.verify(jsInterpreterMock, Mockito.never()).injectVariable(Mockito.eq("project"), Mockito.anyString());
Mockito.verify(jsInterpreterMock).injectStringVariable(Mockito.eq("resourceType"), Mockito.anyString());
Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("resourceType"), Mockito.anyString());
Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("value"), Mockito.anyString());
Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("zone"), Mockito.anyString());
}
@ -291,7 +291,7 @@ public class QuotaManagerImplTest {
Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("account"), Mockito.anyString());
Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("domain"), Mockito.anyString());
Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("project"), Mockito.anyString());
Mockito.verify(jsInterpreterMock).injectStringVariable(Mockito.eq("resourceType"), Mockito.anyString());
Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("resourceType"), Mockito.anyString());
Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("value"), Mockito.anyString());
Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("zone"), Mockito.anyString());
}

View File

@ -39,7 +39,6 @@ import java.util.stream.Collectors;
import javax.inject.Inject;
import com.cloud.utils.DateUtil;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.command.QuotaBalanceCmd;
@ -70,8 +69,8 @@ import org.apache.cloudstack.quota.dao.QuotaCreditsDao;
import org.apache.cloudstack.quota.dao.QuotaEmailConfigurationDao;
import org.apache.cloudstack.quota.dao.QuotaEmailTemplatesDao;
import org.apache.cloudstack.quota.dao.QuotaTariffDao;
import org.apache.cloudstack.quota.vo.QuotaAccountVO;
import org.apache.cloudstack.quota.dao.QuotaUsageDao;
import org.apache.cloudstack.quota.vo.QuotaAccountVO;
import org.apache.cloudstack.quota.vo.QuotaBalanceVO;
import org.apache.cloudstack.quota.vo.QuotaCreditsVO;
import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
@ -79,26 +78,28 @@ import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO;
import org.apache.cloudstack.quota.vo.QuotaTariffVO;
import org.apache.cloudstack.quota.vo.QuotaUsageVO;
import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Component;
import com.cloud.domain.DomainVO;
import com.cloud.domain.dao.DomainDao;
import com.cloud.event.ActionEvent;
import com.cloud.event.EventTypes;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.user.AccountVO;
import com.cloud.user.User;
import com.cloud.user.dao.AccountDao;
import com.cloud.user.dao.UserDao;
import com.cloud.utils.DateUtil;
import com.cloud.utils.Pair;
import com.cloud.utils.db.Filter;
import com.cloud.event.ActionEvent;
import com.cloud.event.EventTypes;
@Component
public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
@ -139,6 +140,12 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
@Inject
private ApiDiscoveryService apiDiscoveryService;
protected void checkActivationRulesAllowed(String activationRule) {
if (!_quotaService.isJsInterpretationEnabled() && StringUtils.isNotEmpty(activationRule)) {
throw new PermissionDeniedException("Quota Tariff Activation Rule cannot be set, as Javascript interpretation is disabled in the configuration.");
}
}
@Override
public QuotaTariffResponse createQuotaTariffResponse(QuotaTariffVO tariff, boolean returnActivationRule) {
final QuotaTariffResponse response = new QuotaTariffResponse();
@ -440,6 +447,8 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
throw new InvalidParameterValueException(String.format("There is no quota tariffs with name [%s].", name));
}
checkActivationRulesAllowed(activationRule);
Date currentQuotaTariffStartDate = currentQuotaTariff.getEffectiveOn();
currentQuotaTariff.setRemoved(now);
@ -696,6 +705,8 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
throw new InvalidParameterValueException(String.format("A quota tariff with name [%s] already exist.", name));
}
checkActivationRulesAllowed(activationRule);
if (startDate.compareTo(now) < 0) {
throw new InvalidParameterValueException(String.format("The value passed as Quota tariff's start date is in the past: [%s]. " +
"Please, inform a date in the future or do not pass the parameter to use the current date and time.", startDate));

View File

@ -16,15 +16,15 @@
//under the License.
package org.apache.cloudstack.quota;
import com.cloud.user.AccountVO;
import com.cloud.utils.component.PluggableService;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import org.apache.cloudstack.quota.vo.QuotaBalanceVO;
import org.apache.cloudstack.quota.vo.QuotaUsageVO;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import com.cloud.user.AccountVO;
import com.cloud.utils.component.PluggableService;
public interface QuotaService extends PluggableService {
@ -40,4 +40,6 @@ public interface QuotaService extends PluggableService {
boolean saveQuotaAccount(AccountVO account, BigDecimal aggrUsage, Date endDate);
boolean isJsInterpretationEnabled();
}

View File

@ -60,6 +60,7 @@ import com.cloud.configuration.Config;
import com.cloud.domain.dao.DomainDao;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.server.ManagementService;
import com.cloud.user.Account;
import com.cloud.user.AccountVO;
import com.cloud.user.dao.AccountDao;
@ -86,6 +87,8 @@ public class QuotaServiceImpl extends ManagerBase implements QuotaService, Confi
private TimeZone _usageTimezone;
private boolean jsInterpretationEnabled = false;
public QuotaServiceImpl() {
super();
}
@ -97,6 +100,8 @@ public class QuotaServiceImpl extends ManagerBase implements QuotaService, Confi
String timeZoneStr = ObjectUtils.defaultIfNull(_configDao.getValue(Config.UsageAggregationTimezone.toString()), "GMT");
_usageTimezone = TimeZone.getTimeZone(timeZoneStr);
jsInterpretationEnabled = ManagementService.JsInterpretationEnabled.value();
return true;
}
@ -284,4 +289,8 @@ public class QuotaServiceImpl extends ManagerBase implements QuotaService, Confi
}
}
@Override
public boolean isJsInterpretationEnabled() {
return jsInterpretationEnabled;
}
}

View File

@ -154,6 +154,7 @@ import com.cloud.org.Cluster;
import com.cloud.org.Grouping;
import com.cloud.org.Managed;
import com.cloud.serializer.GsonHelper;
import com.cloud.server.ManagementService;
import com.cloud.service.ServiceOfferingVO;
import com.cloud.service.dao.ServiceOfferingDao;
import com.cloud.service.dao.ServiceOfferingDetailsDao;
@ -271,6 +272,8 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager,
private ServiceOfferingDetailsDao _serviceOfferingDetailsDao;
@Inject
private UserVmManager userVmManager;
@Inject
ManagementService managementService;
private List<? extends Discoverer> _discoverers;
@ -1936,6 +1939,9 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager,
@Override
public Host updateHost(final UpdateHostCmd cmd) throws NoTransitionException {
managementService.checkJsInterpretationAllowedIfNeededForParameterValue(ApiConstants.IS_TAG_A_RULE,
Boolean.TRUE.equals(cmd.getIsTagARule()));
return updateHost(cmd.getId(), cmd.getName(), cmd.getOsCategoryId(),
cmd.getAllocationState(), cmd.getUrl(), cmd.getHostTags(), cmd.getIsTagARule(), cmd.getAnnotation(), false);
}

View File

@ -1040,6 +1040,8 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
protected List<DeploymentPlanner> _planners;
private boolean jsInterpretationEnabled = false;
private final List<HypervisorType> supportedHypervisors = new ArrayList<>();
public List<DeploymentPlanner> getPlanners() {
@ -1126,6 +1128,8 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
supportedHypervisors.add(HypervisorType.KVM);
supportedHypervisors.add(HypervisorType.XenServer);
jsInterpretationEnabled = JsInterpretationEnabled.value();
return true;
}
@ -4022,8 +4026,10 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
cmdList.add(ListGuestVlansCmd.class);
cmdList.add(AssignVolumeCmd.class);
cmdList.add(ListSecondaryStorageSelectorsCmd.class);
cmdList.add(CreateSecondaryStorageSelectorCmd.class);
cmdList.add(UpdateSecondaryStorageSelectorCmd.class);
if (jsInterpretationEnabled) {
cmdList.add(CreateSecondaryStorageSelectorCmd.class);
cmdList.add(UpdateSecondaryStorageSelectorCmd.class);
}
cmdList.add(RemoveSecondaryStorageSelectorCmd.class);
cmdList.add(ListAffectedVmsForStorageScopeChangeCmd.class);
@ -4066,7 +4072,8 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
@Override
public ConfigKey<?>[] getConfigKeys() {
return new ConfigKey<?>[] {vmPasswordLength, sshKeyLength, humanReadableSizes, customCsIdentifier};
return new ConfigKey<?>[] {vmPasswordLength, sshKeyLength, humanReadableSizes, customCsIdentifier,
JsInterpretationEnabled};
}
protected class EventPurgeTask extends ManagedContextRunnable {
@ -5523,4 +5530,13 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
_lockControllerListener = lockControllerListener;
}
@Override
public void checkJsInterpretationAllowedIfNeededForParameterValue(String paramName, boolean paramValue) {
if (!paramValue || jsInterpretationEnabled) {
return;
}
throw new InvalidParameterValueException(String.format(
"The parameter %s cannot be set to true as JS interpretation is disabled",
paramName));
}
}

View File

@ -213,6 +213,7 @@ import com.cloud.org.Grouping.AllocationState;
import com.cloud.resource.ResourceState;
import com.cloud.server.ConfigurationServer;
import com.cloud.server.ManagementServer;
import com.cloud.server.ManagementService;
import com.cloud.server.StatsCollector;
import com.cloud.service.dao.ServiceOfferingDetailsDao;
import com.cloud.storage.Storage.ImageFormat;
@ -398,6 +399,8 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C
ConfigurationDao configurationDao;
@Inject
private ImageStoreDetailsUtil imageStoreDetailsUtil;
@Inject
ManagementService managementService;
protected List<StoragePoolDiscoverer> _discoverers;
@ -1015,6 +1018,9 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C
throw new PermissionDeniedException(String.format("Cannot perform this operation, Zone is currently disabled: %s", zone));
}
managementService.checkJsInterpretationAllowedIfNeededForParameterValue(ApiConstants.IS_TAG_A_RULE,
Boolean.TRUE.equals(cmd.isTagARule()));
Map<String, Object> params = new HashMap<>();
params.put("zoneId", zone.getId());
params.put("clusterId", clusterId);
@ -1197,6 +1203,9 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C
// Input validation
Long id = cmd.getId();
managementService.checkJsInterpretationAllowedIfNeededForParameterValue(ApiConstants.IS_TAG_A_RULE,
Boolean.TRUE.equals(cmd.isTagARule()));
StoragePoolVO pool = _storagePoolDao.findById(id);
if (pool == null) {
throw new IllegalArgumentException("Unable to find storage pool with ID: " + id);

View File

@ -19,31 +19,52 @@ package org.apache.cloudstack.utils.jsinterpreter;
import java.io.Closeable;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.apache.commons.collections.MapUtils;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import javax.script.SimpleBindings;
import javax.script.SimpleScriptContext;
import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.commons.collections.MapUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.openjdk.nashorn.api.scripting.ClassFilter;
import org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory;
import javax.script.ScriptEngine;
import com.cloud.utils.exception.CloudRuntimeException;
/**
* A class to execute JavaScript scripts, with the possibility to inject context to the scripts.
* Executes JavaScript with strong restrictions to mitigate RCE risks.
* - Disables Java interop via --no-java AND a deny-all ClassFilter
* - Disables Nashorn syntax extensions
* - Uses Bindings instead of string-splicing variables
* - Fresh ScriptContext per execution, with timeout on a daemon worker
*/
public class JsInterpreter implements Closeable {
protected Logger logger = LogManager.getLogger(JsInterpreter.class);
protected static final List<String> RESTRICTED_TOKENS = Arrays.asList( "engine", "context", "factory",
"Java", "java", "Packages"," javax", "load", "loadWithNewGlobal", "print", "factory", "getClass",
"runCommand", "Runtime", "exec", "ProcessBuilder", "Thread", "thread", "Threads", "Class", "class");
protected ScriptEngine interpreter;
protected String interpreterName;
private final String injectingLogMessage = "Injecting variable [%s] with value [%s] into the JS interpreter.";
@ -51,21 +72,40 @@ public class JsInterpreter implements Closeable {
private TimeUnit defaultTimeUnit = TimeUnit.MILLISECONDS;
private long timeout;
private String timeoutDefaultMessage;
protected Map<String, String> variables = new LinkedHashMap<>();
// Store variables as Objects; they go into Bindings (no code splicing)
protected Map<String, Object> variables = new LinkedHashMap<>();
/** Deny-all filter: no Java class is visible from scripts. */
static final class DenyAllClassFilter implements ClassFilter {
@Override public boolean exposeToScripts(String className) { return false; }
}
/**
* Constructor created exclusively for unit testing.
*/
protected JsInterpreter() {
}
protected JsInterpreter() { }
public JsInterpreter(long timeout) {
this.timeout = timeout;
this.timeoutDefaultMessage = String.format("Timeout (in milliseconds) defined in the global setting [quota.activationrule.timeout]: [%s].", this.timeout);
this.timeoutDefaultMessage = String.format(
"Timeout (in milliseconds) defined in the global setting [quota.activationrule.timeout]: [%s].", this.timeout);
if (System.getProperty("nashorn.args") == null) {
System.setProperty("nashorn.args", "--no-java --no-syntax-extensions");
}
this.executor = new ThreadPoolExecutor(
1, 1, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
r -> {
Thread t = new Thread(r, "JsInterpreter-worker");
t.setDaemon(true);
return t;
}
);
executor = Executors.newSingleThreadExecutor();
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
this.interpreterName = factory.getEngineName();
logger.trace(String.format("Initiating JS interpreter: %s.", interpreterName));
@ -73,49 +113,53 @@ public class JsInterpreter implements Closeable {
}
protected void setScriptEngineDisablingJavaLanguage(NashornScriptEngineFactory factory) {
interpreter = factory.getScriptEngine("--no-java");
String[] opts = new String[] {
"--no-java",
"--no-syntax-extensions",
};
interpreter = factory.getScriptEngine(
opts,
JsInterpreter.class.getClassLoader(),
new DenyAllClassFilter()
);
}
/**
* Discards the current variables map and create a new one.
*/
/** Discards the current variables map and create a new one. */
public void discardCurrentVariables() {
logger.trace("Discarding current variables map and creating a new one.");
variables = new LinkedHashMap<>();
}
/**
* Adds the parameters to a Map that will be converted to JS variables right before executing the script.
* @param key The name of the variable.
* @param value The value of the variable.
* Adds a variable that will be exposed via ENGINE_SCOPE bindings.
* Safe against code injection (no string concatenation).
*/
public void injectVariable(String key, String value) {
logger.trace(String.format(injectingLogMessage, key, value));
public void injectVariable(String key, Object value) {
if (key == null) return;
logger.trace(String.format(injectingLogMessage, key, String.valueOf(value)));
variables.put(key, value);
}
/**
* Adds the parameter, surrounded by double quotes, to a Map that will be converted to a JS variable right before executing the script.
* @param key The name of the variable.
* @param value The value of the variable.
* @deprecated Not needed when using Bindings; kept for source compatibility.
* Prefer {@link #injectVariable(String, Object)}.
*/
@Deprecated
public void injectStringVariable(String key, String value) {
if (value == null) {
logger.trace(String.format("Not injecting [%s] because its value is null.", key));
return;
}
value = String.format("\"%s\"", value);
logger.trace(String.format(injectingLogMessage, key, value));
variables.put(key, value);
injectVariable(key, value);
}
/**
* Injects the variables to the script and execute it.
* Injects the variables via Bindings and executes the script with a fresh context.
* @param script Code to be executed.
* @return The result of the executed script.
*/
public Object executeScript(String script) {
script = addVariablesToScript(script);
Objects.requireNonNull(script, "script");
logger.debug(String.format("Executing script [%s].", script));
@ -126,43 +170,60 @@ public class JsInterpreter implements Closeable {
}
protected Object executeScriptInThread(String script) {
Callable<Object> task = () -> interpreter.eval(script);
final Callable<Object> task = () -> {
final SimpleScriptContext ctx = new SimpleScriptContext();
Future<Object> future = executor.submit(task);
final Bindings engineBindings = new SimpleBindings();
if (MapUtils.isNotEmpty(variables)) {
engineBindings.putAll(variables);
}
for (String token : RESTRICTED_TOKENS) {
engineBindings.put(token, null);
}
ctx.setBindings(engineBindings, ScriptContext.ENGINE_SCOPE);
final StringWriter out = new StringWriter();
ctx.setWriter(out);
try {
final CompiledScript compiled = ((Compilable) interpreter).compile(script);
Object result = compiled.eval(ctx);
if (out.getBuffer().length() > 0) {
logger.info("Script produced output on stdout: [{}]", out);
}
return result;
} catch (ScriptException se) {
String msg = se.getMessage() == null ? "Script error" : se.getMessage();
throw new ScriptException("Script error: " + msg, se.getFileName(), se.getLineNumber(), se.getColumnNumber());
}
};
final Future<Object> future = executor.submit(task);
try {
return future.get(this.timeout, defaultTimeUnit);
} catch (TimeoutException | InterruptedException | ExecutionException e) {
String message = String.format("Unable to execute script [%s] due to [%s]", script, e.getMessage());
if (e instanceof TimeoutException) {
message = String.format("Execution of script [%s] took too long and timed out. %s", script, timeoutDefaultMessage);
}
} catch (TimeoutException e) {
String message = String.format(
"Execution of script [%s] took too long and timed out. %s", script, timeoutDefaultMessage);
logger.error(message, e);
throw new CloudRuntimeException(message, e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
String message = String.format("Execution of script [%s] was interrupted.", script);
logger.error(message, e);
throw new CloudRuntimeException(message, e);
} catch (ExecutionException e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
String message = String.format("Unable to execute script [%s] due to [%s]", script, cause.getMessage());
logger.error(message, cause);
throw new CloudRuntimeException(message, cause);
} finally {
future.cancel(true);
}
}
protected String addVariablesToScript(String script) {
if (MapUtils.isEmpty(variables)) {
logger.trace(String.format("There is no variables to add to script [%s]. Returning.", script));
return script;
}
String variablesToString = "";
for (Map.Entry<String, String> variable : variables.entrySet()) {
variablesToString = String.format("%s %s = %s;", variablesToString, variable.getKey(), variable.getValue());
}
return String.format("%s %s", variablesToString, script);
}
@Override
public void close() throws IOException {
executor.shutdown();
executor.shutdownNow();
}
}

View File

@ -16,12 +16,12 @@
//under the License.
package org.apache.cloudstack.utils.jsinterpreter;
import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.commons.lang3.StringEscapeUtils;
import java.io.IOException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import com.cloud.utils.exception.CloudRuntimeException;
public class TagAsRuleHelper {
@ -32,7 +32,6 @@ public class TagAsRuleHelper {
public static boolean interpretTagAsRule(String rule, String tags, long timeout) {
String script = PARSE_TAGS + rule;
tags = String.format("'%s'", StringEscapeUtils.escapeEcmaScript(tags));
try (JsInterpreter jsInterpreter = new JsInterpreter(timeout)) {
jsInterpreter.injectVariable("tags", tags);
Object scriptReturn = jsInterpreter.executeScript(script);

View File

@ -20,6 +20,7 @@ package org.apache.cloudstack.utils.jsinterpreter;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
@ -27,7 +28,8 @@ import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;
import com.cloud.utils.exception.CloudRuntimeException;
import javax.script.ScriptEngine;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -36,9 +38,10 @@ import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import org.openjdk.nashorn.api.scripting.ClassFilter;
import org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory;
import javax.script.ScriptEngine;
import com.cloud.utils.exception.CloudRuntimeException;
@RunWith(MockitoJUnitRunner.class)
public class JsInterpreterTest {
@ -61,30 +64,6 @@ public class JsInterpreterTest {
Assert.assertTrue(executor.isShutdown());
}
@Test
public void addVariablesToScriptTestVariablesMapIsEmptyReturnScript() {
String script = "a + b";
jsInterpreterSpy.variables = new LinkedHashMap<>();
String result = jsInterpreterSpy.addVariablesToScript(script);
Assert.assertEquals(script, result);
}
@Test
public void addVariablesToScriptTestVariablesMapIsNotEmptyInjectVariableToScript() {
String script = "a + b";
String var1 = "{test: \"test\"}";
jsInterpreterSpy.injectVariable("var1", var1);
jsInterpreterSpy.injectVariable("var2", var1);
String expected = String.format(" var1 = %s; var2 = %s; %s", var1, var1, script);
String result = jsInterpreterSpy.addVariablesToScript(script);
Assert.assertEquals(expected, result);
}
@Test
public void executeScriptTestReturnResultOfScriptExecution() {
String script = "5";
@ -154,7 +133,7 @@ public class JsInterpreterTest {
@Test
public void discardCurrentVariablesTestInstantiateNewMap() {
Map<String, String> variables = new LinkedHashMap<>();
Map<String, Object> variables = new LinkedHashMap<>();
variables.put("a", "b");
variables.put("b", null);
@ -170,12 +149,14 @@ public class JsInterpreterTest {
NashornScriptEngineFactory nashornScriptEngineFactoryMock = Mockito.spy(NashornScriptEngineFactory.class);
ScriptEngine scriptEngineMock = Mockito.mock(ScriptEngine.class);
Mockito.doReturn(scriptEngineMock).when(nashornScriptEngineFactoryMock).getScriptEngine(Mockito.anyString());
Mockito.doReturn(scriptEngineMock).when(nashornScriptEngineFactoryMock).getScriptEngine(Mockito.any(),
Mockito.any(ClassLoader.class), Mockito.any(ClassFilter.class));
jsInterpreterSpy.setScriptEngineDisablingJavaLanguage(nashornScriptEngineFactoryMock);
Assert.assertEquals(scriptEngineMock, jsInterpreterSpy.interpreter);
Mockito.verify(nashornScriptEngineFactoryMock).getScriptEngine("--no-java");
Mockito.verify(nashornScriptEngineFactoryMock).getScriptEngine(Mockito.any(),
Mockito.any(ClassLoader.class), Mockito.any(ClassFilter.class));
}
@Test
@ -193,6 +174,46 @@ public class JsInterpreterTest {
jsInterpreterSpy.injectStringVariable("a", "b");
Assert.assertEquals(jsInterpreterSpy.variables.get("a"), "\"b\"");
Assert.assertEquals(jsInterpreterSpy.variables.get("a"), "b");
}
@Test
public void executeScriptTestValidScriptShouldPassWithMixedVariables() {
try (JsInterpreter jsInterpreter = new JsInterpreter(1000)) {
jsInterpreter.injectVariable("x", 10);
jsInterpreter.injectVariable("y", "hello");
jsInterpreter.injectVariable("z", true);
String validScript = "var result = x + (z ? 1 : 0); y + '-' + result;";
Object result = jsInterpreter.executeScript(validScript);
Assert.assertEquals("hello-11", result);
} catch (IOException exception) {
Assert.fail("IOException not expected here");
}
}
private void runMaliciousScriptFileTest(String script, String filename) {
try (JsInterpreter jsInterpreter = new JsInterpreter(1000)) {
jsInterpreter.executeScript(script);
} catch (CloudRuntimeException ex) {
Assert.assertTrue(ex.getMessage().contains("Unable to execute script"));
java.io.File f = new java.io.File(filename);
Assert.assertFalse(f.exists());
} catch (IOException exception) {
Assert.fail("IOException not expected here");
}
}
@Test
public void executeScriptTestMaliciousScriptShouldThrowException1() {
String filename = "/tmp/hack1-" + UUID.randomUUID();
String maliciousScript = "Java.type('java.lang.Runtime').getRuntime().exec('touch " + filename + "')";
runMaliciousScriptFileTest(maliciousScript, filename);
}
@Test
public void executeScriptTestMaliciousScriptShouldThrowException2() {
String filename = "/tmp/hack2-" + UUID.randomUUID();
String maliciousScript = "var e=this.engine.getFactory().getScriptEngine('-Dnashorn.args=--no-java=False'); e.eval(\"java.lang.Runtime.getRuntime().exec(['/bin/bash','-c','touch " + filename + "']);\");";
runMaliciousScriptFileTest(maliciousScript, filename);
}
}