Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SpEL vararg method invocation fails if string literal contains a comma #27582

Closed
bmoraes-axur opened this issue Oct 20, 2021 · 12 comments
Closed
Assignees
Labels
in: core Issues in core modules (aop, beans, core, context, expression) type: bug A general bug
Milestone

Comments

@bmoraes-axur
Copy link

bmoraes-axur commented Oct 20, 2021

Affects: 4.1.2 and up

I'm trying to use my own matching functions with spring-expression, but I came across a possible bug.

Here's some code for context:

import org.springframework.expression.spel.support.StandardEvaluationContext;

public class RuleEvaluationContext extends StandardEvaluationContext {

	public RuleEvaluationContext(Object rootObject) {
		super(rootObject);
		registerFunctions();
	}

	public RuleEvaluationContext() {
		super();
		registerFunctions();
	}

	private void registerFunctions() {
		for (Functions fun : Functions.values()) {
			this.registerFunction(fun.funcName(), fun.method());
		}
	}

}
enum Functions {
/* Functions commented for the sake of simplicity

	CONTAINS(Contains.FUNC_NAME,
		getDeclaredMethod(Contains.class, Contains.FUNC_NAME, String.class, String[].class)),

	STARTS_WITH(StartsWith.FUNC_NAME,
		getDeclaredMethod(StartsWith.class, StartsWith.FUNC_NAME, String.class, String[].class)),
*/

	MATCHES(Matches.FUNC_NAME,
		getDeclaredMethod(Matches.class, Matches.FUNC_NAME, String.class, String[].class)),
/*
	EQUALS(Equals.FUNC_NAME,
		getDeclaredMethod(Equals.class, Equals.FUNC_NAME, String.class, String.class)),

	GREATER(GreaterThan.FUNC_NAME,
		getDeclaredMethod(GreaterThan.class, GreaterThan.FUNC_NAME, String.class, String.class)),

	LESS_THAN(LessThan.FUNC_NAME,
		getDeclaredMethod(LessThan.class, LessThan.FUNC_NAME, String.class, String.class));
*/

	private final String funcName;
	private final Method method;

	Functions(String funcName, Method method) {
		this.funcName = funcName;
		this.method = method;
	}

	public String funcName() {
		return funcName;
	}

	public Method method() {
		return method;
	}

	private static Method getDeclaredMethod(Class c, String name, Class<?>... parameterTypes) {
		try {
			return c.getDeclaredMethod(name, parameterTypes);
		} catch (NoSuchMethodException e) {
			throw new FunctionsException(e);
		}
	}

}

This is one of the registered functions:

public class Matches {

	public static final String FUNC_NAME = "matches";

	private Matches() {
		super();
	}

	public static boolean matches(String text, String... regexps) {
		if (text != null) {
			for (String regex : regexps) {
				Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
				Matcher matcher = pattern.matcher(text);
				if (matcher.find()) {
					return true;
				}
			}
		}
		return false;
	}
}
@Test
public void test() {
	final RuleEvaluationContext context = new RuleEvaluationContext(new FakeObject());
	String originalRule = "#matches(prop, 'xyz,xyz')";
	Expression expression = parser.parseExpression(originalRule);
	Boolean result = expression.getValue(context, Boolean.class);
	assertThat(result, is(false));
}

private final ExpressionParser parser = new SpelExpressionParser();

private static class FakeObject {
	private String prop = "xyz";

	public String getProp() {
		return prop;
	}
}

This test fails when it should pass. Upon further inspection I've found that the problem is that spring-expression breaks the 'xyz,xyz' into ["xyz", "xyz"] as can be seen below.

libnova

However I've also found that using old versions of the library (pre 4.1.2), this doesn't happen.

libantiga

As does putting more strings inside the argument:

@Test
public void test() {
	final RuleEvaluationContext context = new RuleEvaluationContext(new FakeObject());
	String originalRule = "#matches(prop, 'abc', 'xyz,xyz')";
	Expression expression = parser.parseExpression(originalRule);
	Boolean result = expression.getValue(context, Boolean.class);
	assertThat(result, is(false));
}

libnovasembug

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Oct 20, 2021
@sbrannen sbrannen added the in: core Issues in core modules (aop, beans, core, context, expression) label Oct 20, 2021
@sbrannen
Copy link
Member

sbrannen commented Oct 21, 2021

@sbrannen sbrannen added type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Oct 21, 2021
@sbrannen sbrannen added this to the 5.3.13 milestone Oct 21, 2021
@sbrannen
Copy link
Member

I have confirmed that this is a bug which applies to method invocations in general (not just to custom functions).

@sbrannen sbrannen changed the title Spring-expression comma parsing issue SpEL vararg method invocation fails if string literal contains a comma Oct 21, 2021
@sbrannen sbrannen self-assigned this Oct 21, 2021
@xixingya
Copy link
Contributor

hello, I have find it is because if just two args, the second args will have another logic

@xixingya
Copy link
Contributor

xixingya commented Oct 22, 2021

first into org.springframework.expression.spel.ast.FunctionReference#executeFunctionJLRMethod.
and in ReflectionHelper.convertAllArguments. org.springframework.expression.spel.support.ReflectionHelper#convertArguments


if (varargsPosition == arguments.length - 1) {
				// If the target is varargs and there is just one more argument
				// then convert it here
				TypeDescriptor targetType = new TypeDescriptor(methodParam);
				Object argument = arguments[varargsPosition];
				TypeDescriptor sourceType = TypeDescriptor.forObject(argument);
				arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetType);
				// Three outcomes of that previous line:
				// 1) the input argument was already compatible (ie. array of valid type) and nothing was done
				// 2) the input argument was correct type but not in an array so it was made into an array
				// 3) the input argument was the wrong type and got converted and put into an array
				if (argument != arguments[varargsPosition] &&
						!isFirstEntryInArray(argument, arguments[varargsPosition])) {
					conversionOccurred = true; // case 3
				}
			}

@xixingya
Copy link
Contributor

xixingya commented Oct 22, 2021

but I have no idea to fix the problem, in the doc say it is a feature if just one argument should convert
// If the target is varargs and there is just one more argument
// then convert it here

@sbrannen
Copy link
Member

Thanks for sharing your thoughts, @xixingya.

I already had a fix in place and pushed it to 5.3.x and main.

@sbrannen
Copy link
Member

@bmoraes-axur, feel free to try this out in the next 5.3.13 snapshot and let us know if you run into any issues.

sbrannen added a commit to sbrannen/spring-framework that referenced this issue Oct 22, 2021
@bmoraes-axur
Copy link
Author

@bmoraes-axur, feel free to try this out in the next 5.3.13 snapshot and let us know if you run into any issues.

Will do, thank you very much!

sbrannen added a commit that referenced this issue Dec 10, 2021
A regression was introduced in gh-27582. Specifically, when null is
supplied as the single argument for a varargs parameter in a method or
function in a SpEL expression, ReflectionHelper currently throws a
NullPointerException instead of leaving the null value unchanged.

This commit fixes this regression.

Closes gh-27719
@Jos31fr
Copy link

Jos31fr commented Jan 6, 2022

Warning: I had a regression on my web app I think due to this correction. I think it's correct but some persons like me can be impacted with a minor evolution of spring (12 -> 13).

In Spring Security, I was using @PreAuthorize("hasAnyRole('ROLE_X, ROLE_Y')") without quoting each ROLE. I must now use @PreAuthorize("hasAnyRole('ROLE_X', 'ROLE_Y')").

@sbrannen
Copy link
Member

sbrannen commented Jan 6, 2022

Warning: I had a regression on my web app I think due to this correction. I think it's correct but some persons like me can be impacted with a minor evolution of spring (12 -> 13).

That's a good point. If you were relying on this bug, you'll need to switch to the proper syntax for multiple vararg values.

In Spring Security, I was using @PreAuthorize("hasAnyRole('ROLE_X, ROLE_Y')") without quoting each ROLE. I must now use @PreAuthorize("hasAnyRole('ROLE_X', 'ROLE_Y')").

That is indeed the correct way to use hasAnyRole() as documented in the Spring Security reference docs.

@bbbush
Copy link

bbbush commented Jan 22, 2022

In Spring Security, I was using @PreAuthorize("hasAnyRole('ROLE_X, ROLE_Y')") without quoting each ROLE. I must now use @PreAuthorize("hasAnyRole('ROLE_X', 'ROLE_Y')").

we encountered the same regression when upgrading from earlier versions. We use a property to store the list of authorized roles or groups, and now due to this change, entire list is treated as one role -- and "hasAnyAuthority" is failing because of that.

@PreAuthorize("hasAnyAuthority(@propertyResolver.getProperty('admin.authority'))")

Because a property is a string, this regression only impacts varargs uses like hasAnyAuthority

some workaround suggested from @yabqiu: split the array in the expression, or add another wrapper to help infer the correct type.

@SpringBootApplication
public class Main implements CommandLineRunner {

    static {
        System.setProperty("property1", "{'a','b'}");
    }

    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }

    public static int setProperty(String... args) {
        return args.length;
    }

    public static String[] identity(String[] arr) {
        return arr;
    }

    @Autowired
    public void printLength(@Value("#{@main.setProperty(systemProperties['property1'])}") int value) {
        System.out.println("property length: " + value);
    }

    @Autowired
    public void printLengthSplit(@Value("#{T(test.Main).setProperty(systemProperties['property1'].split(','))}") int value) {
        System.out.println("length with split: " + value);
    }

    @Autowired
    public void printLengthId(@Value("#{T(test.Main).setProperty(T(test.Main).identity(systemProperties['property1'] ))}") int value) {
        System.out.println("length with type cast: " + value);
    }

    @Override
    public void run(String... args) throws Exception {
    }
}

@manoharank
Copy link

manoharank commented Oct 20, 2023

@sbrannen Still facing the issue when the method is print(Object... args) and invoking print(name) and name has comma. No issue when invoking print(name, age) (more than one arguments).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core Issues in core modules (aop, beans, core, context, expression) type: bug A general bug
Projects
None yet
Development

No branches or pull requests

7 participants