Unauthenticated remote code injection in cron-utils
Description
Template injection in cron-utils @Cron annotation allows unauthenticated RCE; fixed in 9.1.6.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Template injection in cron-utils @Cron annotation allows unauthenticated RCE; fixed in 9.1.6.
Vulnerability
The vulnerability is a Java EL template injection in cron-utils versions up to 9.1.2. The library parses cron expressions for validation. When using the @Cron annotation to validate untrusted Cron expressions, the expression string is incorrectly included in error messages, allowing injection of arbitrary Java EL expressions. [1]
Exploitation
An attacker can provide a malicious cron expression containing EL expressions (e.g., using ${...} syntax). If the expression fails validation, the library’s error message includes the original expression string, which gets evaluated by the template engine. This requires no authentication and only that the application uses the @Cron annotation on an untrusted input. [1][3]
Impact
Successful exploitation leads to unauthenticated Remote Code Execution (RCE) on the server, as the attacker can execute arbitrary Java code via EL injection. This compromises confidentiality, integrity, and availability. [1]
Mitigation
The issue is fixed in version 9.1.6, released on 2021-11-15. Users should upgrade to 9.1.6 or later. There are no known workarounds. The fix removed the expression from the error message, preventing injection via crafted validation failures. [1][3]
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
com.cronutils:cron-utilsMaven | < 9.1.6 | 9.1.6 |
Affected products
2- Range: < 9.1.6
Patches
2cfd2880f80e6Merge pull request #494 from NielsDoucet/RCE-fix
4 files changed · +18 −9
src/main/java/com/cronutils/parser/CronParser.java+1 −1 modified@@ -128,7 +128,7 @@ public Cron parse(final String expression) { } return new SingleCron(cronDefinition, results).validate(); } catch (final IllegalArgumentException e) { - throw new IllegalArgumentException(String.format("Failed to parse '%s'. %s", expression, e.getMessage()), e); + throw new IllegalArgumentException(String.format("Failed to parse cron expression. %s", e.getMessage()), e); } } }
src/test/java/com/cronutils/Issue418Test.java+5 −4 modified@@ -5,6 +5,7 @@ import com.cronutils.model.definition.CronDefinitionBuilder; import com.cronutils.model.time.ExecutionTime; import com.cronutils.parser.CronParser; +import org.hamcrest.core.StringEndsWith; import org.junit.Test; import java.time.LocalDate; @@ -13,8 +14,8 @@ import java.time.ZonedDateTime; import java.util.Optional; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; +import static org.hamcrest.core.StringEndsWith.endsWith; +import static org.junit.Assert.*; public class Issue418Test { @@ -59,7 +60,7 @@ public void testInvalidWeekDayStart() { parser.parse("0 0 2 ? * 0/7 *"); fail("Expected exception for invalid expression"); } catch (IllegalArgumentException expected) { - assertEquals("Failed to parse '0 0 2 ? * 0/7 *'. Value 0 not in range [1, 7]", expected.getMessage()); + assertThat(expected.getMessage(), endsWith("Value 0 not in range [1, 7]")); } } @@ -71,7 +72,7 @@ public void testInvalidWeekDayEnd() { parser.parse("0 0 2 ? * 1/8 *"); fail("Expected exception for invalid expression"); } catch (IllegalArgumentException expected) { - assertEquals("Failed to parse '0 0 2 ? * 1/8 *'. Period 8 not in range [1, 7]", expected.getMessage()); + assertThat(expected.getMessage(), endsWith("Period 8 not in range [1, 7]")); } } }
src/test/java/com/cronutils/parser/CronParserQuartzIntegrationTest.java+4 −3 modified@@ -20,16 +20,19 @@ import com.cronutils.model.definition.CronDefinitionBuilder; import com.cronutils.model.field.expression.FieldExpressionFactory; import com.cronutils.model.time.ExecutionTime; +import org.hamcrest.core.StringEndsWith; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.internal.matchers.ThrowableMessageMatcher; import org.junit.rules.ExpectedException; import java.time.ZonedDateTime; import java.util.Locale; import java.util.Optional; import static org.junit.Assert.*; +import static org.junit.internal.matchers.ThrowableMessageMatcher.hasMessage; public class CronParserQuartzIntegrationTest { @@ -248,9 +251,7 @@ public void testReportedErrorContainsSameExpressionAsProvided() { public void testMissingExpressionAndInvalidCharsInErrorMessage() { thrown.expect(IllegalArgumentException.class); final String cronexpression = "* * -1 * * ?"; - thrown.expectMessage( - String.format("Failed to parse '%s'. Invalid expression! Expression: -1 does not describe a range. Negative numbers are not allowed.", - cronexpression)); + thrown.expect(hasMessage(StringEndsWith.endsWith("Invalid expression! Expression: -1 does not describe a range. Negative numbers are not allowed."))); assertNotNull(ExecutionTime.forCron(parser.parse(cronexpression))); }
src/test/java/com/cronutils/validation/CronValidatorTest.java+8 −1 modified@@ -4,6 +4,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.validation.ConstraintViolation; import javax.validation.Validation; @@ -16,6 +18,8 @@ @RunWith(Parameterized.class) public class CronValidatorTest { + private static final Logger LOGGER = LoggerFactory.getLogger(CronValidatorTest.class); + private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); private final String expression; @@ -38,14 +42,17 @@ public static Object[] expressions() { {"0 0 0 25 12 ?", true}, {"0 0 0 L 12 ?", false}, {"1,2, * * * * *", false}, - {"1- * * * * *", false} + {"1- * * * * *", false}, + // Verification for RCE security vulnerability fix: https://github.com/jmrozanec/cron-utils/issues/461 + {"java.lang.Runtime.getRuntime().exec('touch /tmp/pwned'); // 4 5 [${''.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('js').eval(validatedValue)}]", false} }; } @Test public void validateExamples() { TestPojo testPojo = new TestPojo(expression); Set<ConstraintViolation<TestPojo>> violations = validator.validate(testPojo); + violations.stream().map(ConstraintViolation::getMessage).forEach(LOGGER::info); if (valid) { assertTrue(violations.isEmpty());
d6707503ec2fMerge pull request #493 from pwntester/patch-1
1 file changed · +1 −1
src/main/java/com/cronutils/validation/CronValidator.java+1 −1 modified@@ -30,7 +30,7 @@ public boolean isValid(String value, ConstraintValidatorContext context) { return true; } catch (IllegalArgumentException e) { context.disableDefaultConstraintViolation(); - context.buildConstraintViolationWithTemplate(e.getMessage()).addConstraintViolation(); + context.buildConstraintViolationWithTemplate("Error parsing the Cron expression").addConstraintViolation(); return false; } }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-p9m8-27x8-rg87ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-41269ghsaADVISORY
- github.com/jmrozanec/cron-utils/commit/cfd2880f80e62ea74b92fa83474c2aabdb9899daghsax_refsource_MISCWEB
- github.com/jmrozanec/cron-utils/commit/d6707503ec2f20947f79e38f861dba93b39df9daghsax_refsource_MISCWEB
- github.com/jmrozanec/cron-utils/issues/461ghsax_refsource_MISCWEB
- github.com/jmrozanec/cron-utils/security/advisories/GHSA-p9m8-27x8-rg87ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.