How One Line of Code Opened a Remote Code Execution Hole in SpringBoot

A SpringBoot project’s custom validator introduced a severe remote code execution vulnerability when a single line of code interpolated user input, illustrating the importance of rigorous input validation, internationalized error handling, and security scanning before deployment.

Programmer DD
Programmer DD
Programmer DD
How One Line of Code Opened a Remote Code Execution Hole in SpringBoot

Background

In a global‑user SpringBoot web project we built a unified exception handling component that centralises error codes and messages, using internationalised resource files to return locale‑specific messages.

Internationalised Error Example

When a verification code expires we throw throw new ErrorCodeException(ErrorCodes.AUTHCODE_EXPIRED). The message is then looked up in language files, e.g.:

Chinese: "您输入的验证码已过期,请重新获取"

English: "The verification code you input is expired, ..."

German: "Der von Ihnen eingegebene Verifizierungscode ist abgelaufen, bitte wiederholen"

The core of this mechanism is a GlobalExceptionHandler that intercepts all controller exceptions and retrieves the appropriate i18n message.

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(BadRequestException.class)
    @ResponseBody
    public ResponseEntity handle(HttpServletRequest request, BadRequestException e) {
        String i18message = getI18nMessage(e.getKey(), request);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Response.error(e.getCode(), i18message));
    }

    @ExceptionHandler(ErrorCodeException.class)
    @ResponseBody
    public ResponseEntity handle(HttpServletRequest request, ErrorCodeException e) {
        String i18message = getI18nMessage(e.getKey(), request);
        return ResponseEntity.status(HttpStatus.OK).body(Response.error(e.getCode(), i18message));
    }
}

Form Validation with Custom Annotations

We defined a UserRegForm class and added constraints:

public class UserRegForm {
    @Length(min = 6, max = 20, message = "validate.userRegForm.nickname")
    private String nickname;

    @Gender(message = "validate.userRegForm.gender")
    private String gender;

    @NotNull
    @Email(message = "validate.userRegForm.email")
    private String email;
}

The custom @Gender annotation is backed by a validator that checks the value against an allowed list.

@Documented
@Constraint(validatedBy = CustomValidator.class)
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomParam {
    String message() default "validate.custom.default";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    @interface List { CustomParam[] value(); }
}
public class CustomValidator implements ConstraintValidator<CustomParam, String> {
    @Override
    public boolean isValid(String s, ConstraintValidatorContext ctx) {
        if (s == null || s.isEmpty()) return true;
        if (s.equals("tanglei")) return true;
        error(ctx, "Invalid params: " + s);
        return false;
    }
    private static void error(ConstraintValidatorContext ctx, String msg) {
        ctx.disableDefaultConstraintViolation();
        ctx.buildConstraintViolationWithTemplate(msg).addConstraintViolation();
    }
}

Security Incident

Before release the security team scanned the code and discovered that the custom validator’s error message was built by concatenating the raw user input:

error(ctx, "Invalid params: " + s);

This allowed an attacker to inject a Spring Expression Language (SpEL) expression such as 1+1=${1+1}. The expression was interpolated by Hibernate Validator’s message interpolator, leading to evaluation of arbitrary EL code and ultimately remote code execution (e.g., executing ls -al or ping www.tanglei.name).

The root cause was the unchecked interpolation of user‑supplied strings in the validation error message, which the validator treated as a message template and evaluated.

Lessons Learned

Never trust user input; always validate and sanitize before using it in message templates or code.

Apply the principle of least privilege to the runtime user of your services.

Use automated security scanning tools to catch such vulnerabilities early.

Keep internationalised messages static or use safe placeholders instead of raw user data.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Exception HandlingSpringBootinternationalizationSecurity VulnerabilityValidatorEL Injection
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.