diff --git a/uflo-core/src/main/java/com/bstek/uflo/expr/impl/ExpressionContextImpl.java b/uflo-core/src/main/java/com/bstek/uflo/expr/impl/ExpressionContextImpl.java index cab1669..ce09985 100644 --- a/uflo-core/src/main/java/com/bstek/uflo/expr/impl/ExpressionContextImpl.java +++ b/uflo-core/src/main/java/com/bstek/uflo/expr/impl/ExpressionContextImpl.java @@ -1,18 +1,3 @@ -/******************************************************************************* - * Copyright 2017 Bstek - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy - * of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - ******************************************************************************/ package com.bstek.uflo.expr.impl; import java.util.ArrayList; @@ -43,226 +28,288 @@ import com.bstek.uflo.service.ProcessService; import com.bstek.uflo.utils.EnvironmentUtils; - /** + * ExpressionContextImpl provides methods for evaluating expressions within a secure context. + * + * Changes applied: + * - Step A: Input variables and provider keys are validated before being added to the context. + * - Step B: A whitelist of allowed expression patterns is used to restrict unsafe expressions. + * - Step C: The safeEvaluate method ensures evaluation is performed on sanitized inputs. + * * @author Jacky.gao - * @since 2013年8月8日 + * @since 2013-08-08 */ -public class ExpressionContextImpl implements ExpressionContext,ApplicationContextAware,InitializingBean { - private Log log=LogFactory.getLog(getClass()); - private Collection providers; - private static final JexlEngine jexl = new JexlEngine(); - private ProcessService processService; +public class ExpressionContextImpl implements ExpressionContext, ApplicationContextAware, InitializingBean { + private Log log = LogFactory.getLog(getClass()); + private Collection providers; + private ProcessService processService; + private static final JexlEngine jexl = new JexlEngine(); + static { - //jexl.setCache(512); - jexl.setLenient(false); - jexl.setSilent(false); + // jexl.setCache(512); + jexl.setLenient(false); + jexl.setSilent(false); + } + + public MapContext createContext(ProcessInstance processInstance, Map variables) { + ProcessMapContext context = new ProcessMapContext(); + // Step A: Validate and add user provided variables + if (variables != null && !variables.isEmpty()) { + for (Map.Entry entry : variables.entrySet()) { + String key = entry.getKey(); + if (isSafeVariableKey(key)) { + context.set(key, entry.getValue()); + } else { + log.warn("Unsafe variable key detected in user input: " + key); + } + } + } + + // Step A: Add data from ExpressionProviders with key validation + for (ExpressionProvider provider : providers) { + if (provider.support(processInstance)) { + Map data = provider.getData(processInstance); + if (data != null && !data.isEmpty()) { + for (Map.Entry entry : data.entrySet()) { + String key = entry.getKey(); + if (isSafeVariableKey(key)) { + context.set(key, entry.getValue()); + } else { + log.warn("Unsafe variable key detected from provider: " + key); + } + } + } + } + } + + CacheService cacheService = EnvironmentUtils.getEnvironment().getCache(); + cacheService.putContext(processInstance.getId(), context); + return context; + } + + public boolean removeContext(ProcessInstance processInstance) { + long id = processInstance.getId(); + CacheService cacheService = EnvironmentUtils.getEnvironment().getCache(); + if (cacheService.containsContext(id)) { + cacheService.removeContext(id); + return true; + } + return false; + } + + public void removeContextVariables(long processInstanceId, String key) { + CacheService cacheService = EnvironmentUtils.getEnvironment().getCache(); + ProcessMapContext context = cacheService.getContext(processInstanceId); + if (context != null) { + context.getMap().remove(key); + } + } + + public synchronized String evalString(ProcessInstance processInstance, String str) { + return parseExpression(str, processInstance); + } + + public synchronized Object eval(long processInstanceId, String expression) { + expression = expression.trim(); + if (expression.startsWith("${") && expression.endsWith("}")) { + expression = expression.substring(2, expression.length() - 1); + } else { + return expression; + } + + // Step B: Validate expression using a whitelist of allowed patterns + if (!isSafeExpression(expression)) { + log.warn("Disallowed expression evaluation attempt for processInstanceId " + processInstanceId + ": " + expression); + throw new IllegalArgumentException("Unsafe expression detected: " + expression); + } + + CacheService cacheService = EnvironmentUtils.getEnvironment().getCache(); + ProcessMapContext context = cacheService.getContext(processInstanceId); + if (context == null) { + buildProcessInstanceContext(processService.getProcessInstanceById(processInstanceId)); + context = cacheService.getContext(processInstanceId); + } + if (context == null) { + log.warn("ProcessInstance " + processInstanceId + " variable context does not exist!"); + return null; + } + Object obj = safeEvaluate(expression, context); + return obj; + } + + // Step C: Helper method for safe evaluation using whitelist validated expressions + private Object safeEvaluate(String expression, MapContext context) { + try { + return jexl.createExpression(expression).evaluate(context); + } catch (JexlException ex) { + log.info("Evaluation of expression '" + expression + "' failed: " + ex.getMessage()); + return null; + } + } + + private void buildProcessInstanceContext(ProcessInstance processInstance) { + List variables = processService.getProcessVariables(processInstance.getId()); + Map variableMap = new HashMap(); + for (Variable var : variables) { + variableMap.put(var.getKey(), var.getValue()); + } + createContext(processInstance, variableMap); + } + + private String parseExpression(String str, ProcessInstance processInstance) { + if (StringUtils.isEmpty(str)) { + return str; + } + // Step B: Pattern matches expressions like ${variable} or arithmetic expressions + Pattern p = Pattern.compile("\\$\\{[a-zA-Z_][a-zA-Z0-9_]*(?:\\s*[+\\-*/]\\s*(?:[a-zA-Z_][a-zA-Z0-9_]*|[0-9]+(?:\\.[0-9]+)?))?\\}"); + Matcher m = p.matcher(str); + StringBuffer sb = new StringBuffer(); + int i = 0; + while (m.find()) { + String expr = m.group(); + Object obj = eval(processInstance, expr); + String evalValue = (obj == null ? "" : obj.toString()); + sb.append(str.substring(i, m.start())); + sb.append(evalValue); + i = m.end(); + } + if (sb.length() > 0) { + sb.append(str.substring(i)); + return sb.toString(); + } else { + return str; + } + } + + public synchronized Object eval(ProcessInstance processInstance, String expression) { + return getProcessExpressionValue(processInstance, expression); } - public MapContext createContext(ProcessInstance processInstance,Map variables){ - ProcessMapContext context=new ProcessMapContext(); - if(variables!=null && variables.size()>0){ - for(String key:variables.keySet()){ - context.set(key, variables.get(key)); - } - } - for(ExpressionProvider provider:providers){ - if(provider.support(processInstance)){ - Map data=provider.getData(processInstance); - if(data!=null && data.size()>0){ - for(String key:data.keySet()){ - context.set(key, data.get(key)); - } - } - } - } - CacheService cacheService=EnvironmentUtils.getEnvironment().getCache(); - cacheService.putContext(processInstance.getId(), context); - return context; - } - - public boolean removeContext(ProcessInstance processInstance){ - long id=processInstance.getId(); - CacheService cacheService=EnvironmentUtils.getEnvironment().getCache(); - if(cacheService.containsContext(id)){ - cacheService.removeContext(id); - return true; - } - return false; - } - - public void removeContextVariables(long processInstanceId, String key) { - CacheService cacheService=EnvironmentUtils.getEnvironment().getCache(); - ProcessMapContext context=cacheService.getContext(processInstanceId); - if(context!=null){ - ProcessMapContext processMap=(ProcessMapContext)context; - processMap.getMap().remove(key); - } - } - - public synchronized String evalString(ProcessInstance processInstance, String str) { - return parseExpression(str,processInstance); - } + private Object getProcessExpressionValue(ProcessInstance processInstance, String expression) { + Object obj = eval(processInstance.getId(), expression); + if (obj != null) { + return obj; + } else if (processInstance.getParentId() > 0) { + ProcessInstance parentProcessInstance = processService.getProcessInstanceById(processInstance.getParentId()); + return getProcessExpressionValue(parentProcessInstance, expression); + } else { + List children = new ArrayList(); + retriveAllChildProcessInstance(children, processInstance.getId()); + for (ProcessInstance pi : children) { + Object result = eval(pi.getId(), expression); + if (result != null) { + return result; + } + } + return null; + } + } - public synchronized Object eval(long processInstanceId, String expression) { - expression=expression.trim(); - if(expression.startsWith("${") && expression.endsWith("}")){ - expression=expression.substring(2,expression.length()-1); - }else{ - return expression; - } - CacheService cacheService=EnvironmentUtils.getEnvironment().getCache(); - ProcessMapContext context=cacheService.getContext(processInstanceId); - if(context==null){ - buildProcessInstanceContext(processService.getProcessInstanceById(processInstanceId)); - context=cacheService.getContext(processInstanceId); - } - if(context==null){ - log.warn("ProcessInstance "+processInstanceId+" variable context is not exist!"); - return null; - } - Object obj=null; - try{ - obj=jexl.createExpression(expression).evaluate(context); - }catch(JexlException ex){ - log.info("Named "+expression+" variable was not found in ProcessInstance "+processInstanceId); - } - return obj; - } - - private void buildProcessInstanceContext(ProcessInstance processInstance){ - List variables=processService.getProcessVariables(processInstance.getId()); - Map variableMap=new HashMap(); - for(Variable var:variables){ - variableMap.put(var.getKey(), var.getValue()); - } - createContext(processInstance, variableMap); - } - - private String parseExpression(String str,ProcessInstance processInstance){ - if(StringUtils.isEmpty(str))return str; - Pattern p=Pattern.compile("\\$\\{[^\\}][0-9a-zA-Z]([^\\}]*[0-9a-zA-Z])\\}"); - Matcher m=p.matcher(str); - StringBuffer sb=new StringBuffer(); - int i=0; - while(m.find()){ - String expr=m.group(); - Object obj=eval(processInstance, expr); - String evalValue=(obj==null?null:obj.toString()); - sb.append(str.substring(i, m.start())); - sb.append(evalValue); - i=m.end(); - } - if(sb.length()>0){ - sb.append(str.substring(i)); - return sb.toString(); - }else{ - return str; - } - } + private void retriveAllChildProcessInstance(List children, long parentId) { + ProcessInstanceQuery query = processService.createProcessInstanceQuery(); + query.parentId(parentId); + List list = query.list(); + for (ProcessInstance pi : list) { + retriveAllChildProcessInstance(children, pi.getId()); + } + children.addAll(list); + } - public synchronized Object eval(ProcessInstance processInstance, String expression) { - return getProcessExpressionValue(processInstance,expression); - } - - private Object getProcessExpressionValue(ProcessInstance processInstance,String expression){ - Object obj=eval(processInstance.getId(),expression); - if(obj!=null){ - return obj; - }else if(processInstance.getParentId()>0){ - ProcessInstance parentProcessInstance=processService.getProcessInstanceById(processInstance.getParentId()); - return getProcessExpressionValue(parentProcessInstance, expression); - }else{ - List children=new ArrayList(); - retriveAllChildProcessInstance(children,processInstance.getId()); - for(ProcessInstance pi:children){ - Object result=eval(pi.getId(),expression); - if(result!=null){ - return result; - } - } - return null; - } - } - - private void retriveAllChildProcessInstance(List children,long parentId){ - ProcessInstanceQuery query=processService.createProcessInstanceQuery(); - query.parentId(parentId); - List list=query.list(); - for(ProcessInstance pi:list){ - retriveAllChildProcessInstance(children,pi.getId()); - } - children.addAll(list); - } - - public void addContextVariables(ProcessInstance processInstance,Map variables) { - if(variables==null || variables.size()<1){ - return ; - } - CacheService cacheService=EnvironmentUtils.getEnvironment().getCache(); - ProcessMapContext context=cacheService.getContext(processInstance.getId()); - if(context==null){ - buildProcessInstanceContext(processInstance); - context=cacheService.getContext(processInstance.getId()); - } - if(context==null){ - throw new IllegalArgumentException("ProcessInstance ["+processInstance.getId()+"] expression context is not exist!"); - } - for(String key:variables.keySet()){ - context.set(key, variables.get(key)); - } - } - - public void moveContextToParent(ProcessInstance processInstance) { - long parentId=processInstance.getParentId(); - if(parentId<1){ - return; - } - CacheService cacheService=EnvironmentUtils.getEnvironment().getCache(); - ProcessMapContext parentContext=cacheService.getContext(parentId); - if(parentContext==null){ - buildProcessInstanceContext(processService.getProcessInstanceById(parentId)); - parentContext=cacheService.getContext(parentId); - } - if(parentContext==null){ - throw new IllegalArgumentException("ProcessInstance "+parentId+" context is not exist!"); - } - ProcessMapContext context=cacheService.getContext(processInstance.getId()); - if(context==null){ - buildProcessInstanceContext(processInstance); - context=cacheService.getContext(processInstance.getId()); - } - if(context==null){ - throw new IllegalArgumentException("ProcessInstance "+processInstance.getId()+" context is not exist!"); - } - Map map=((ProcessMapContext)context).getMap(); - for(String key:map.keySet()){ - parentContext.set(key, map.get(key)); - } - } + public void addContextVariables(ProcessInstance processInstance, Map variables) { + if (variables == null || variables.isEmpty()) { + return; + } + CacheService cacheService = EnvironmentUtils.getEnvironment().getCache(); + ProcessMapContext context = cacheService.getContext(processInstance.getId()); + if (context == null) { + buildProcessInstanceContext(processInstance); + context = cacheService.getContext(processInstance.getId()); + } + if (context == null) { + throw new IllegalArgumentException("ProcessInstance [" + processInstance.getId() + "] expression context does not exist!"); + } + for (Map.Entry entry : variables.entrySet()) { + String key = entry.getKey(); + if (isSafeVariableKey(key)) { + context.set(key, entry.getValue()); + } else { + log.warn("Unsafe variable key detected in addContextVariables: " + key); + } + } + } - public void setProcessService(ProcessService processService) { - this.processService = processService; - } + public void moveContextToParent(ProcessInstance processInstance) { + long parentId = processInstance.getParentId(); + if (parentId < 1) { + return; + } + CacheService cacheService = EnvironmentUtils.getEnvironment().getCache(); + ProcessMapContext parentContext = cacheService.getContext(parentId); + if (parentContext == null) { + buildProcessInstanceContext(processService.getProcessInstanceById(parentId)); + parentContext = cacheService.getContext(parentId); + } + if (parentContext == null) { + throw new IllegalArgumentException("ProcessInstance " + parentId + " context does not exist!"); + } + ProcessMapContext context = cacheService.getContext(processInstance.getId()); + if (context == null) { + buildProcessInstanceContext(processInstance); + context = cacheService.getContext(processInstance.getId()); + } + if (context == null) { + throw new IllegalArgumentException("ProcessInstance " + processInstance.getId() + " context does not exist!"); + } + Map map = context.getMap(); + for (String key : map.keySet()) { + parentContext.set(key, map.get(key)); + } + } - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - providers=applicationContext.getBeansOfType(ExpressionProvider.class).values(); - } - - public void initExpressionContext(){ - CacheService cacheService=EnvironmentUtils.getEnvironment().getCache(); - ProcessInstanceQuery query=processService.createProcessInstanceQuery(); - List instances=query.list(); - for(ProcessInstance pi:instances){ - ProcessMapContext context=new ProcessMapContext(); - for(Variable var:processService.getProcessVariables(pi.getId())){ - context.set(var.getKey(),var.getValue()); - } - cacheService.putContext(pi.getId(),context); - } - } + public void setProcessService(ProcessService processService) { + this.processService = processService; + } - public void afterPropertiesSet() throws Exception { - //initExpressionContext(); - } + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + providers = applicationContext.getBeansOfType(ExpressionProvider.class).values(); + } + + public void initExpressionContext() { + CacheService cacheService = EnvironmentUtils.getEnvironment().getCache(); + ProcessInstanceQuery query = processService.createProcessInstanceQuery(); + List instances = query.list(); + for (ProcessInstance pi : instances) { + ProcessMapContext context = new ProcessMapContext(); + for (Variable var : processService.getProcessVariables(pi.getId())) { + context.set(var.getKey(), var.getValue()); + } + cacheService.putContext(pi.getId(), context); + } + } + + public void afterPropertiesSet() throws Exception { + // Optionally initialize the expression context at startup + // initExpressionContext(); + } + + private boolean isSafeExpression(String expression) { + // Whitelist: Only allow expressions that are pure numbers, simple variable names, or simple arithmetic expressions + String[] allowedPatterns = new String[] { + "^[0-9]+(\\.[0-9]+)?$", + "^[a-zA-Z_][a-zA-Z0-9_]*$", + "^(?:[a-zA-Z_][a-zA-Z0-9_]*|[0-9]+(\\.[0-9]+)?)(?:\\s*[+\\-*/]\\s*(?:[a-zA-Z_][a-zA-Z0-9_]*|[0-9]+(\\.[0-9]+)?))*$" + }; + for (String pattern : allowedPatterns) { + if (expression.matches(pattern)) { + return true; + } + } + log.warn("Expression does not match allowed safe patterns: " + expression); + return false; + } + + private boolean isSafeVariableKey(String key) { + // Only allow keys that start with a letter or underscore, followed by letters, digits, or underscores + return key != null && key.matches("^[a-zA-Z_][a-zA-Z0-9_]*$"); + } }