diff --git a/repository/src/main/java/org/alfresco/repo/jscript/AlfrescoContextFactory.java b/repository/src/main/java/org/alfresco/repo/jscript/AlfrescoContextFactory.java
new file mode 100644
index 0000000000..c7bbb055fb
--- /dev/null
+++ b/repository/src/main/java/org/alfresco/repo/jscript/AlfrescoContextFactory.java
@@ -0,0 +1,201 @@
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2022 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.jscript;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.mozilla.javascript.Callable;
+import org.mozilla.javascript.Context;
+import org.mozilla.javascript.ContextFactory;
+import org.mozilla.javascript.Scriptable;
+
+/**
+ * Custom factory that allows to apply configured limits during script executions
+ *
+ * @see ContextFactory
+ */
+public class AlfrescoContextFactory extends ContextFactory
+{
+ private static final Log LOGGER = LogFactory.getLog(AlfrescoContextFactory.class);
+
+ private int optimizationLevel = -1;
+ private int maxScriptExecutionSeconds = -1;
+ private int maxStackDepth = -1;
+ private long maxMemoryUsedInBytes = -1L;
+ private int observeInstructionCount = -1;
+
+ private AlfrescoScriptThreadMxBeanWrapper threadMxBeanWrapper;
+
+ private final int INTERPRETIVE_MODE = -1;
+
+ @Override
+ protected Context makeContext()
+ {
+ AlfrescoScriptContext context = new AlfrescoScriptContext();
+
+ context.setOptimizationLevel(optimizationLevel);
+
+ // Needed for both time and memory measurement
+ if (maxScriptExecutionSeconds > 0 || maxMemoryUsedInBytes > 0L)
+ {
+ if (observeInstructionCount > 0)
+ {
+ LOGGER.info("Enabling observer count...");
+ context.setGenerateObserverCount(true);
+ context.setInstructionObserverThreshold(observeInstructionCount);
+ }
+ else
+ {
+ LOGGER.info("Disabling observer count...");
+ context.setGenerateObserverCount(false);
+ }
+ }
+
+ // Memory limit
+ if (maxMemoryUsedInBytes > 0)
+ {
+ context.setThreadId(Thread.currentThread().getId());
+ }
+
+ // Max stack depth
+ if (maxStackDepth > 0)
+ {
+ if (optimizationLevel != INTERPRETIVE_MODE)
+ {
+ LOGGER.warn("Changing optimization level from " + optimizationLevel + " to " + INTERPRETIVE_MODE);
+ }
+ // stack depth can only be set when no optimizations are applied
+ context.setOptimizationLevel(INTERPRETIVE_MODE);
+ context.setMaximumInterpreterStackDepth(maxStackDepth);
+ }
+
+ return context;
+ }
+
+ @Override
+ protected void observeInstructionCount(Context cx, int instructionCount)
+ {
+ AlfrescoScriptContext acx = (AlfrescoScriptContext) cx;
+
+ if (acx.isLimitsEnabled())
+ {
+ // Time limit
+ if (maxScriptExecutionSeconds > 0)
+ {
+ long currentTime = System.currentTimeMillis();
+ if (currentTime - acx.getStartTime() > maxScriptExecutionSeconds * 1000)
+ {
+ throw new Error("Maximum script time of " + maxScriptExecutionSeconds + " seconds exceeded");
+ }
+ }
+
+ // Memory
+ if (maxMemoryUsedInBytes > 0 && threadMxBeanWrapper != null && threadMxBeanWrapper.isThreadAllocatedMemorySupported())
+ {
+
+ if (acx.getStartMemory() <= 0)
+ {
+ acx.setStartMemory(threadMxBeanWrapper.getThreadAllocatedBytes(acx.getThreadId()));
+ }
+ else
+ {
+ long currentAllocatedBytes = threadMxBeanWrapper.getThreadAllocatedBytes(acx.getThreadId());
+ if (currentAllocatedBytes - acx.getStartMemory() >= maxMemoryUsedInBytes)
+ {
+ throw new Error("Memory limit of " + maxMemoryUsedInBytes + " bytes reached");
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ protected Object doTopCall(Callable callable, Context cx, Scriptable scope, Scriptable thisObj, Object[] args)
+ {
+ AlfrescoScriptContext acx = (AlfrescoScriptContext) cx;
+ acx.setStartTime(System.currentTimeMillis());
+ return super.doTopCall(callable, cx, scope, thisObj, args);
+ }
+
+ public int getOptimizationLevel()
+ {
+ return optimizationLevel;
+ }
+
+ public void setOptimizationLevel(int optimizationLevel)
+ {
+ this.optimizationLevel = optimizationLevel;
+ }
+
+ public int getMaxScriptExecutionSeconds()
+ {
+ return maxScriptExecutionSeconds;
+ }
+
+ public void setMaxScriptExecutionSeconds(int maxScriptExecutionSeconds)
+ {
+ this.maxScriptExecutionSeconds = maxScriptExecutionSeconds;
+ }
+
+ public int getMaxStackDepth()
+ {
+ return maxStackDepth;
+ }
+
+ public void setMaxStackDepth(int maxStackDepth)
+ {
+ this.maxStackDepth = maxStackDepth;
+ }
+
+ public long getMaxMemoryUsedInBytes()
+ {
+ return maxMemoryUsedInBytes;
+ }
+
+ public void setMaxMemoryUsedInBytes(long maxMemoryUsedInBytes)
+ {
+ this.maxMemoryUsedInBytes = maxMemoryUsedInBytes;
+ if (maxMemoryUsedInBytes > 0)
+ {
+ this.threadMxBeanWrapper = new AlfrescoScriptThreadMxBeanWrapper();
+ if (!threadMxBeanWrapper.isThreadAllocatedMemorySupported())
+ {
+ LOGGER.warn("com.sun.management.ThreadMXBean was not found on the classpath. "
+ + "This means that the limiting the memory usage for a script will NOT work.");
+ }
+ }
+ }
+
+ public int getObserveInstructionCount()
+ {
+ return observeInstructionCount;
+ }
+
+ public void setObserveInstructionCount(int observeInstructionCount)
+ {
+ this.observeInstructionCount = observeInstructionCount;
+ }
+}
\ No newline at end of file
diff --git a/repository/src/main/java/org/alfresco/repo/jscript/AlfrescoScriptContext.java b/repository/src/main/java/org/alfresco/repo/jscript/AlfrescoScriptContext.java
new file mode 100644
index 0000000000..cc5a4d6b20
--- /dev/null
+++ b/repository/src/main/java/org/alfresco/repo/jscript/AlfrescoScriptContext.java
@@ -0,0 +1,81 @@
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2022 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.jscript;
+
+import org.mozilla.javascript.Context;
+
+/**
+ * Custom Rhino context that holds data as start time and memory
+ *
+ * @see Context
+ */
+public class AlfrescoScriptContext extends Context
+{
+ private long startTime;
+ private long threadId;
+ private long startMemory;
+ private boolean limitsEnabled = false;
+
+ public long getStartTime()
+ {
+ return startTime;
+ }
+
+ public void setStartTime(long startTime)
+ {
+ this.startTime = startTime;
+ }
+
+ public long getThreadId()
+ {
+ return threadId;
+ }
+
+ public void setThreadId(long threadId)
+ {
+ this.threadId = threadId;
+ }
+
+ public long getStartMemory()
+ {
+ return startMemory;
+ }
+
+ public void setStartMemory(long startMemory)
+ {
+ this.startMemory = startMemory;
+ }
+
+ public boolean isLimitsEnabled()
+ {
+ return limitsEnabled;
+ }
+
+ public void setLimitsEnabled(boolean limitsEnabled)
+ {
+ this.limitsEnabled = limitsEnabled;
+ }
+}
\ No newline at end of file
diff --git a/repository/src/main/java/org/alfresco/repo/jscript/AlfrescoScriptThreadMxBeanWrapper.java b/repository/src/main/java/org/alfresco/repo/jscript/AlfrescoScriptThreadMxBeanWrapper.java
new file mode 100644
index 0000000000..0d2bbd9321
--- /dev/null
+++ b/repository/src/main/java/org/alfresco/repo/jscript/AlfrescoScriptThreadMxBeanWrapper.java
@@ -0,0 +1,78 @@
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2022 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.jscript;
+
+import java.lang.management.ManagementFactory;
+import java.lang.management.ThreadMXBean;
+
+/**
+ * Allows to monitor memory usage
+ */
+public class AlfrescoScriptThreadMxBeanWrapper
+{
+
+ private ThreadMXBean threadMXBean = null;
+ private boolean threadAllocatedMemorySupported = false;
+
+ private final String THREAD_MX_BEAN_SUN = "com.sun.management.ThreadMXBean";
+
+ public AlfrescoScriptThreadMxBeanWrapper()
+ {
+ checkThreadAllocatedMemory();
+ }
+
+ public long getThreadAllocatedBytes(long threadId)
+ {
+ if (threadMXBean != null && threadAllocatedMemorySupported)
+ {
+ return ((com.sun.management.ThreadMXBean) threadMXBean).getThreadAllocatedBytes(threadId);
+ }
+
+ return -1;
+ }
+
+ public void checkThreadAllocatedMemory()
+ {
+ try
+ {
+ Class> clazz = Class.forName(THREAD_MX_BEAN_SUN);
+ if (clazz != null)
+ {
+ this.threadAllocatedMemorySupported = true;
+ this.threadMXBean = (com.sun.management.ThreadMXBean) ManagementFactory.getThreadMXBean();
+ }
+ }
+ catch (Exception e)
+ {
+ this.threadAllocatedMemorySupported = false;
+ }
+ }
+
+ public boolean isThreadAllocatedMemorySupported()
+ {
+ return threadAllocatedMemorySupported;
+ }
+}
\ No newline at end of file
diff --git a/repository/src/main/java/org/alfresco/repo/jscript/RhinoScriptProcessor.java b/repository/src/main/java/org/alfresco/repo/jscript/RhinoScriptProcessor.java
index dee9b5cffc..59728e18b9 100644
--- a/repository/src/main/java/org/alfresco/repo/jscript/RhinoScriptProcessor.java
+++ b/repository/src/main/java/org/alfresco/repo/jscript/RhinoScriptProcessor.java
@@ -57,10 +57,12 @@ import org.alfresco.service.namespace.QName;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mozilla.javascript.Context;
+import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.ImporterTopLevel;
import org.mozilla.javascript.Script;
import org.mozilla.javascript.Scriptable;
-import org.mozilla.javascript.ScriptableObject;
+import org.mozilla.javascript.ScriptableObject;
+import org.mozilla.javascript.Undefined;
import org.mozilla.javascript.WrapFactory;
import org.mozilla.javascript.WrappedException;
import org.springframework.beans.factory.InitializingBean;
@@ -108,7 +110,24 @@ public class RhinoScriptProcessor extends BaseProcessor implements ScriptProcess
/** Cache of runtime compiled script instances */
private final Map scriptCache = new ConcurrentHashMap(256);
-
+ /** Rhino optimization level */
+ private int optimizationLevel = -1;
+
+ /** Maximum seconds a script is allowed to run */
+ private int maxScriptExecutionSeconds = -1;
+
+ /** Maximum of call stack depth (in terms of number of call frames) */
+ private int maxStackDepth = -1;
+
+ /** Maximum memory (bytes) a script can use */
+ private long maxMemoryUsedInBytes = -1L;
+
+ /** Number of (bytecode) instructions that will trigger the observer */
+ private int observerInstructionCount = 100;
+
+ /** Custom context factory */
+ public static AlfrescoContextFactory contextFactory;
+
/**
* Set the default store reference
*
@@ -143,6 +162,51 @@ public class RhinoScriptProcessor extends BaseProcessor implements ScriptProcess
{
this.shareSealedScopes = shareSealedScopes;
}
+
+ /**
+ * @param optimizationLevel
+ * -1 interpretive mode, 0 no optimizations, 1-9 optimizations performed
+ */
+ public void setOptimizationLevel(int optimizationLevel)
+ {
+ this.optimizationLevel = optimizationLevel;
+ }
+
+ /**
+ * @param maxScriptExecutionSeconds
+ * the number of seconds a script is allowed to run
+ */
+ public void setMaxScriptExecutionSeconds(int maxScriptExecutionSeconds)
+ {
+ this.maxScriptExecutionSeconds = maxScriptExecutionSeconds;
+ }
+
+ /**
+ * @param maxStackDepth
+ * the number of call stack depth allowed
+ */
+ public void setMaxStackDepth(int maxStackDepth)
+ {
+ this.maxStackDepth = maxStackDepth;
+ }
+
+ /**
+ * @param maxMemoryUsedInBytes
+ * the number of memory a script can use
+ */
+ public void setMaxMemoryUsedInBytes(long maxMemoryUsedInBytes)
+ {
+ this.maxMemoryUsedInBytes = maxMemoryUsedInBytes;
+ }
+
+ /**
+ * @param observerInstructionCount
+ * the number of instructions that will trigger {@link ContextFactory#observeInstructionCount}
+ */
+ public void setObserverInstructionCount(int observerInstructionCount)
+ {
+ this.observerInstructionCount = observerInstructionCount;
+ }
/**
* @see org.alfresco.service.cmr.repository.ScriptProcessor#reset()
@@ -449,6 +513,8 @@ public class RhinoScriptProcessor extends BaseProcessor implements ScriptProcess
private Object executeScriptImpl(Script script, Map model, boolean secure, String debugScriptName)
throws AlfrescoRuntimeException
{
+ Scriptable scope = null;
+
long startTime = 0;
if (callLogger.isDebugEnabled())
{
@@ -465,14 +531,16 @@ public class RhinoScriptProcessor extends BaseProcessor implements ScriptProcess
// Create a thread-specific scope from one of the shared scopes.
// See http://www.mozilla.org/rhino/scopes.html
cx.setWrapFactory(secure ? wrapFactory : sandboxFactory);
- Scriptable scope;
+
+ // Enables or disables execution limits based on secure flag
+ enableLimits(cx, secure);
+
if (this.shareSealedScopes)
{
Scriptable sharedScope = secure ? this.nonSecureScope : this.secureScope;
scope = cx.newObject(sharedScope);
scope.setPrototype(sharedScope);
scope.setParentScope(null);
-
}
else
{
@@ -545,7 +613,8 @@ public class RhinoScriptProcessor extends BaseProcessor implements ScriptProcess
throw new AlfrescoRuntimeException(err.getMessage(), err);
}
finally
- {
+ {
+ unsetScope(model, scope);
Context.exit();
if (callLogger.isDebugEnabled())
@@ -638,6 +707,9 @@ public class RhinoScriptProcessor extends BaseProcessor implements ScriptProcess
*/
public void afterPropertiesSet() throws Exception
{
+ // Initialize context factory
+ initContextFactory();
+
// Initialize the secure scope
Context cx = Context.enter();
try
@@ -695,4 +767,129 @@ public class RhinoScriptProcessor extends BaseProcessor implements ScriptProcess
}
return scope;
}
+
+ /**
+ * Clean supplied scope and unset it from any model instance where it has been injected before
+ *
+ * @param model
+ * Data model containing objects from where scope will be unset
+ * @param scope
+ * The scope to clean
+ */
+ private void unsetScope(Map model, Scriptable scope)
+ {
+ if (scope != null)
+ {
+ Object[] ids = scope.getIds();
+ if (ids != null)
+ {
+ for (Object id : ids)
+ {
+ try
+ {
+ deleteProperty(scope, id.toString());
+ }
+ catch (Exception e)
+ {
+ logger.info("Unable to delete id: " + id, e);
+ }
+ }
+ }
+ }
+
+ if (model != null)
+ {
+ for (String key : model.keySet())
+ {
+ try
+ {
+ deleteProperty(scope, key);
+
+ Object obj = model.get(key);
+ if (obj instanceof Scopeable)
+ {
+ ((Scopeable) obj).setScope(null);
+ }
+ }
+ catch (Exception e)
+ {
+ logger.info("Unable to unset model object " + key + " : ", e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Deletes a property from the supplied scope, if property is not removable, then is set to null
+ *
+ * @param scope
+ * the scope object from where property will be removed
+ * @param name
+ * the property name to delete
+ */
+ private void deleteProperty(Scriptable scope, String name)
+ {
+ if (scope != null && name != null)
+ {
+ if (!ScriptableObject.deleteProperty(scope, name))
+ {
+ ScriptableObject.putProperty(scope, name, null);
+ }
+ scope.delete(name);
+ }
+ }
+
+ /**
+ * Initializes the context factory with limits configuration
+ */
+ private synchronized void initContextFactory()
+ {
+ if (contextFactory == null)
+ {
+ contextFactory = new AlfrescoContextFactory();
+ contextFactory.setOptimizationLevel(optimizationLevel);
+
+ if (maxScriptExecutionSeconds > 0)
+ {
+ contextFactory.setMaxScriptExecutionSeconds(maxScriptExecutionSeconds);
+ }
+
+ if (maxMemoryUsedInBytes > 0L)
+ {
+ contextFactory.setMaxMemoryUsedInBytes(maxMemoryUsedInBytes);
+ }
+
+ if (maxStackDepth > 0)
+ {
+ contextFactory.setMaxStackDepth(maxStackDepth);
+ }
+
+ if (maxScriptExecutionSeconds > 0 || maxMemoryUsedInBytes > 0L)
+ {
+ contextFactory.setObserveInstructionCount(observerInstructionCount);
+ }
+
+ ContextFactory.initGlobal(contextFactory);
+ }
+ }
+
+ /**
+ * If script is considered secure no limits will be applied, otherwise, the limits are enabled and the script can be
+ * interrupted in case a limit has been reached.
+ *
+ * @param cx
+ * the Rhino scope
+ * @param secure
+ * true if script execution is considered secure (e.g, deployed at classpath level)
+ */
+ private void enableLimits(Context cx, boolean secure)
+ {
+ if (cx != null)
+ {
+ if (cx instanceof AlfrescoScriptContext)
+ {
+ ((AlfrescoScriptContext) cx).setLimitsEnabled(!secure);
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/repository/src/main/resources/alfresco/repository.properties b/repository/src/main/resources/alfresco/repository.properties
index 5ea296393a..43c4d7a69f 100644
--- a/repository/src/main/resources/alfresco/repository.properties
+++ b/repository/src/main/resources/alfresco/repository.properties
@@ -1351,3 +1351,18 @@ import.zip.compressionRatioThreshold=100
# "zip bomb" and the import extraction process cancelled. No value (or a negative long) will be taken to mean that no
# limit should be applied.
import.zip.uncompressedBytesLimit=
+
+# Rhino optimization level
+scripts.execution.optimizationLevel=0
+
+# Max seconds a script is allowed to run
+scripts.execution.maxScriptExecutionSeconds=-1
+
+# Max call stack depth
+scripts.execution.maxStackDepth=-1
+
+# Max memory (bytes) a script can use
+scripts.execution.maxMemoryUsedInBytes=-1
+
+# Number of instructions that will trigger the observer
+scripts.execution.observerInstructionCount=-1
diff --git a/repository/src/main/resources/alfresco/script-services-context.xml b/repository/src/main/resources/alfresco/script-services-context.xml
index 2a2b930696..cbff40693a 100644
--- a/repository/src/main/resources/alfresco/script-services-context.xml
+++ b/repository/src/main/resources/alfresco/script-services-context.xml
@@ -45,6 +45,21 @@
${spaces.company_home.childname}
+
+ ${scripts.execution.optimizationLevel}
+
+
+ ${scripts.execution.maxScriptExecutionSeconds}
+
+
+ ${scripts.execution.maxStackDepth}
+
+
+ ${scripts.execution.maxMemoryUsedInBytes}
+
+
+ ${scripts.execution.observerInstructionCount}
+