[9] Jinjava 3.0: Add method and return type validator framework#1297
[9] Jinjava 3.0: Add method and return type validator framework#1297jasmith-hs merged 8 commits intomasterfrom
Conversation
bf498da to
38c7b0e
Compare
Update some tests Implement allowlisting for methods and classes Extract separate MethodValidator Add result validating Use string-based config. Make the allowlists easier to work with Wrap classes before validating result Split MethodValidator from ReturnTypeValidator Don't create new JinjavaBeanELResolver instances per Jinjava or JinjavaConfig for bean cache performance Validate ReturnTypes at the top-level so that accessing values of arrays, lists, maps have their return values validated Wrap at a higher level and don't resolve `____int3rpr3t3r____` Use BeanMethods and cache allowed methods and return types Don't expect ____int3rpr3t3r____ and don't use arrays in ReverseFilter and add method and return type validator to test classes Helper for constructing JinjavaConfig in tests Extract test objects to separate classes. Allow arrays. Add AnnotationIntrospector Don't need annotation introspector All tests passing Fix build Describe a couple more changes Don't allow jinjava constructs outside of the AllowlistGroup classes from being allowlisted Add tests that certain classes and packages CANNOT be allowlisted Allow BigInteger Add annotation to MethodValidator Test that certain constructs are fully banned Remove unused method from ReturnTypeValidator Use Map and Set instead of ImmutableMap and ImmutableSet for less churn Apply spotless formatting Fix test after merge conflicts Remove 3.0-changes file
- Remove stale commented-out line in ExpressionResolver - Remove @Value.Check from static utility method in BannedAllowlistOptions - Make BeanMethod.method volatile for safe publication across threads - Guard against getBeanMethods returning null for unknown method names - Guard against getCanonicalName returning null for anonymous/local classes in AllowlistReturnTypeValidator and AllowlistMethodValidator Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use instanceof pattern matching in NoInvokeELContext to avoid bare ClassCastException when delegate doesn't implement HasInterpreter - Guard against null JinjavaInterpreter.getCurrent() in SizeLimitingPySet.checkSize Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…javaObjects allowlist group Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ilter for primitive arrays Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…PySet Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
38c7b0e to
da2b67b
Compare
| import java.util.Set; | ||
| import java.util.stream.Stream; | ||
|
|
||
| public class BannedAllowlistOptions { |
There was a problem hiding this comment.
nit: I don't love the name, but it's tolerable. Could be BlocklistOptions or BanlistOptions
There was a problem hiding this comment.
@rewaddell It's banning things from being added to the allowlist.
BlocklistOptions or BanlistOptions don't make sense because that insinuates that this class provides options for a blocklist or for a banlist, which isn't what it does, nor do we allow blocklists in jinjava anymore
BannedAllowlistOptions says: "we are banning you from adding these things to an allowlist"
Alternate suggestions:
BannedAllowlistOptionsAllowlistBanningAllowlistLegalityAllowlistLegalityVerifier
Any pref?
There was a problem hiding this comment.
Ok with that in mind, I do like BannedAllowlistOptions, thanks!
| } | ||
|
|
||
| public Collection<ELFunctionDefinition> getAllFunctions() { | ||
| List<ELFunctionDefinition> fns = new ArrayList<>(functionLibrary.entries()); |
There was a problem hiding this comment.
nit: This could be a HashSet and you could use fns.removeAll(disabledFunctions here or some other set operation
There was a problem hiding this comment.
That isn't strictly better. As creating a HashSet is more expensive than creating an ArrayList.
I'm not really touching this code in this PR. These lines here are changed because I had some intermediate change that switched the disabled library to be ImmutableMap<Library, ImmutableSet<String>>, but I decided against doing that.
I kept these changes in because it's faster to return fns right away if there are no disabled functions rather than loop through it
Part of #1288.
Introduces an allowlist-based validator framework that controls which Java methods can be invoked from Jinja templates and what return types are permitted. This replaces the old blocklist approach (
getRestrictedMethods/getRestrictedProperties/isRestrictedClass) with an opt-in model: only explicitly-permitted methods and return types are allowed through.Why this matters
The old security model in
JinjavaBeanELResolverworked by blocking known-bad things: a hardcoded set of method names (class,clone,hashCode,getClass, etc.), anisRestrictedClasscheck for dangerous types (Class,ClassLoader,Thread,Method,java.lang.reflect.*,com.fasterxml.jackson.databind.*), and optional per-config restricted method/property sets.Blocklists are fundamentally fragile — they require anticipating every dangerous thing. An allowlist inverts the model: unknown things are rejected by default, and only types explicitly categorised as safe are permitted.
New components
Validator interfaces and config
MethodValidator— interface with a singlevalidateMethod(Method)that returnsnullto reject or the method to accept. Supports chaining additional validators.ReturnTypeValidator— same pattern but for return values:validateReturnType(Object).MethodValidatorConfig(@Value.Immutable) — specifies which methods are allowed, by exactMethodinstance, by declaring class canonical name, or by class name prefix. Has anonRejectedMethodcallback for observability. Built viaMethodValidatorConfig.builder().ReturnTypeValidatorConfig(@Value.Immutable) — specifies which return types are allowed, by exact canonical class name or by prefix. Has anonRejectedClasscallback and anallowArraysflag (only enabled for theCollectionsgroup). Built viaReturnTypeValidatorConfig.builder().Both configs enforce hard safety guarantees via
@Value.Check: attempting to addjava.lang.Object,java.lang.Class,java.lang.reflect.*,com.fasterxml.jackson.databind.*, or anycom.hubspot.jinjava.*type outside the explicit safe groups throwsIllegalStateExceptionat construction time. Misconfiguration is a hard error, not a silent runtime bypass. This logic lives inBannedAllowlistOptions.AllowlistGroup enum
Defines named semantic groups used to populate configs:
JavaPrimitivesString,BigDecimal,BigIntegerJinjavaObjectsPyList,PyMap,SizeLimitingPy*,PyishDate,FormattedDate,DummyObject,Namespace,SafeString,NullValueCollectionsPySet,SizeLimitingPySet,ArrayList, Guava forwarding collections,LinkedHashMap; also enables array return typesJinjavaTagConstructsForLoop,MacroFunction,EagerMacroFunctionJinjavaFilterscom.hubspot.jinjava.lib.filter(by package prefix)JinjavaFunctionsZonedDateTime(used by date functions)JinjavaExpTestscom.hubspot.jinjava.lib.exptest(by package prefix)AllowlistMethodValidator.DEFAULTandAllowlistReturnTypeValidator.DEFAULTare pre-built from all groups and used as the defaults inJinjavaConfig.AllowlistMethodValidator / AllowlistReturnTypeValidator
Both validators use
ConcurrentHashMapcaches (keyed onMethodor canonical class name) so the allowlist check is only evaluated once per unique method/type during a JVM session. TheonRejected*callback fires for observability before the method/value is suppressed.ReturnTypeValidatingJinjavaInterpreterResolver
A thin
ELResolverdecorator that wrapsJinjavaInterpreterResolver. AllgetValueandinvokecalls are passed throughAllowlistReturnTypeValidator.validateReturnType()after resolution.ExpressionResolvernow builds this wrapper instead of usingJinjavaInterpreterResolverdirectly.HasInterpreter
A small interface (
JinjavaInterpreter interpreter()) implemented byJinjavaELContextandNoInvokeELContext. Allows the EL resolver layer to obtain the current interpreter from the context without a staticgetCurrent()call when the context is available.Changes to existing components
JinjavaBeanELResolver
The central change. The old
DEFAULT_RESTRICTED_METHODS,DEFAULT_RESTRICTED_PROPERTIES, andisRestrictedClassare removed entirely. Method validation is now delegated:findMethod— after resolving the candidate method, callsgetJinjavaConfig().getMethodValidator().validateMethod(method). Returnsnull(method not found) if the validator rejects it.getReadMethod/getWriteMethod— same pattern, so property access also goes through the allowlist.coerceValue— new override replaces the old____int3rpr3t3r____hack. When a method parameter is typedJinjavaInterpreter, it injectsJinjavaInterpreter.getCurrent()directly. Filters and expression tests that acceptJinjavaInterpreteras a parameter continue to work without any special naming convention.BeanMethodscache (Introspector.getBeanInfo→MethodDescriptor[]indexed by name) per class, stored in aConcurrentHashMap<Class<?>, BeanMethods>. This avoids callinggetClass().getMethods()on every invocation and removes thefindAccessibleMethodscan from the hot path.JinjavaConfig
getRestrictedMethods()andgetRestrictedProperties().getMethodValidator()(default:AllowlistMethodValidator.DEFAULT).getReturnTypeValidator()(default:AllowlistReturnTypeValidator.DEFAULT).Both new methods have
@Value.Defaultso existingJinjavaConfigbuilders continue to work without changes.JinjavaInterpreterResolver
Raw
Setvalues are now wrapped inSizeLimitingPySet(analogous to howListandMapare already wrapped in theirSizeLimiting*equivalents). This also meansSetis now in theCollectionsallowlist group and can be returned from expressions.PySet / SizeLimitingPySet
PySet— a newForwardingSet<Object>that implementsPyWrapper. Includes a recursion-guardedhashCode()to handle sets that contain themselves.SizeLimitingPySet— mirrorsSizeLimitingPyList/SizeLimitingPyMap. EnforcesmaxSizeon construction,add, andaddAll; emits aWARNING-levelTemplateErrorwhen the set reaches 90% capacity.ArrayBacked
Marker interface (
T[] backingArray()) for types that expose a backing array, used to support array return types through the validator.Tests
ValidatorConfigBannedConstructsTest— verifies that banned types (Object,Class,java.lang.reflect,com.fasterxml.jackson.databind,JinjavaInterpreter) cannot be added to eitherMethodValidatorConfigorReturnTypeValidatorConfigwithout throwing.JinjavaBeanELResolverTest— restructured to test the new allowlist-delegation behavior rather than the removed hardcoded blocklist.BaseJinjavaTestand most other tests updated to use explicit validator config where tests exercise method calls that go through the allowlist.