-
Notifications
You must be signed in to change notification settings - Fork 482
fix(velocity): harden SecureIntrospector restricted-class coverage #37549
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,8 +19,23 @@ | |
| * under the License. | ||
| */ | ||
|
|
||
| import java.io.File; | ||
| import java.io.InputStream; | ||
| import java.io.OutputStream; | ||
| import java.io.RandomAccessFile; | ||
| import java.io.Reader; | ||
| import java.io.Writer; | ||
| import java.lang.reflect.Method; | ||
| import java.net.URI; | ||
| import java.net.URL; | ||
| import java.nio.channels.Channel; | ||
| import java.nio.file.FileSystem; | ||
| import java.nio.file.Files; | ||
| import java.nio.file.Path; | ||
| import java.nio.file.Paths; | ||
| import java.nio.file.spi.FileSystemProvider; | ||
|
|
||
| import com.dotmarketing.util.Config; | ||
| import com.dotmarketing.util.Logger; | ||
|
|
||
| /** | ||
|
|
@@ -59,6 +74,7 @@ public SecureIntrospectorImpl(String[] badClasses, String[] badPackages) | |
| * @return Method object retrieved by Introspector | ||
| * @throws IllegalArgumentException The parameter passed in were incorrect. | ||
| */ | ||
| @Override | ||
| public Method getMethod(Class clazz, String methodName, Object[] params) | ||
| throws IllegalArgumentException | ||
| { | ||
|
|
@@ -86,6 +102,7 @@ public Method getMethod(Class clazz, String methodName, Object[] params) | |
| * @param methodName Name of method to be called | ||
| * @see org.apache.velocity.util.introspection.SecureIntrospectorControl#checkObjectExecutePermission(java.lang.Class, java.lang.String) | ||
| */ | ||
| @Override | ||
| public boolean checkObjectExecutePermission(Class clazz, String methodName) | ||
| { | ||
| /** | ||
|
|
@@ -136,6 +153,18 @@ else if (Class.class.isAssignableFrom(clazz) && | |
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Extend restricted-class coverage to the file, IO and network-resource type families. | ||
| * These are matched by type hierarchy so that concrete platform implementations (for | ||
| * example the JDK's internal Path implementation) are covered as well, which an | ||
| * exact-string class/package list cannot reach. The static utility holders Files and | ||
| * Paths are final and have no restricted supertype, so they are matched by identity. | ||
| */ | ||
| if (isRestrictedResourceType(clazz)) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
|
|
||
|
|
||
| /** | ||
|
|
@@ -167,6 +196,138 @@ else if (Class.class.isAssignableFrom(clazz) && | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Operator-configurable restricted packages/classes (additive to the code-level floor | ||
| * above). This lets deployments push the sandbox toward a stricter, package-level stance | ||
| * as new holes are found, without a code change — for example | ||
| * {@code DOT_VELOCITY_INTROSPECTOR_RESTRICT_PACKAGES=java.nio,java.net}. An allow-list | ||
| * (ALLOW_CLASSES_PROP) carves specific classes back out of that configurable denial. | ||
| * Denials are logged by getMethod(). Defaults are empty, so behavior is unchanged unless | ||
| * configured. | ||
| */ | ||
| if (isConfigRestricted(className, packageName)) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| /** dotCMS Config key (env: DOT_VELOCITY_INTROSPECTOR_RESTRICT_PACKAGES): extra restricted package prefixes. */ | ||
| public static final String RESTRICT_PACKAGES_PROP = "velocity.introspector.restrict.packages"; | ||
|
|
||
| /** dotCMS Config key (env: DOT_VELOCITY_INTROSPECTOR_RESTRICT_CLASSES): extra restricted class names. */ | ||
| public static final String RESTRICT_CLASSES_PROP = "velocity.introspector.restrict.classes"; | ||
|
|
||
| /** dotCMS Config key (env: DOT_VELOCITY_INTROSPECTOR_ALLOW_CLASSES): classes carved out of the configurable denial. */ | ||
| public static final String ALLOW_CLASSES_PROP = "velocity.introspector.allow.classes"; | ||
|
|
||
| private static final String[] EMPTY = new String[0]; | ||
|
|
||
| /** | ||
| * Reads a comma-separated dotCMS Config list, tolerating an uninitialized Config so this | ||
| * vendored introspector stays usable outside a running dotCMS (e.g. plain unit tests). | ||
| * | ||
| * @param prop the Config/env property name | ||
| * @return the configured values, or an empty array if unset or unavailable | ||
| */ | ||
| private static String[] configList(final String prop) | ||
| { | ||
| try | ||
| { | ||
| final String[] values = Config.getStringArrayProperty(prop, EMPTY); | ||
| return values == null ? EMPTY : values; | ||
| } | ||
| catch (final Throwable t) | ||
| { | ||
| return EMPTY; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Determines whether the given class is denied by the operator-configurable layer. An | ||
| * explicit allow-list entry wins, so a broadened package can still exempt specific classes. | ||
| * This layer never overrides the code-level floor or the reflection/system denials above. | ||
| * | ||
| * @param className fully-qualified class name (array markers already stripped) | ||
| * @param packageName package of the class ("" for the default package) | ||
| * @return {@code true} if the configurable layer denies the class, {@code false} otherwise | ||
| */ | ||
| private static boolean isConfigRestricted(final String className, final String packageName) | ||
| { | ||
| for (final String allowed : configList(ALLOW_CLASSES_PROP)) | ||
| { | ||
| if (className.equals(allowed.trim())) | ||
| { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| for (final String badClass : configList(RESTRICT_CLASSES_PROP)) | ||
| { | ||
| if (className.equals(badClass.trim())) | ||
| { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| for (final String badPackage : configList(RESTRICT_PACKAGES_PROP)) | ||
| { | ||
| final String prefix = badPackage.trim(); | ||
| if (!prefix.isEmpty() | ||
| && (packageName.equals(prefix) || packageName.startsWith(prefix + "."))) | ||
| { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * The supertypes whose method calls are restricted. Matching by hierarchy means every | ||
| * concrete subtype and implementation is covered without enumerating platform-internal | ||
| * class names. | ||
| */ | ||
| private static final Class<?>[] RESTRICTED_SUPERTYPES = { | ||
| File.class, Path.class, RandomAccessFile.class, InputStream.class, | ||
| OutputStream.class, Reader.class, Writer.class, Channel.class, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ⚪ [P3] SecureIntrospectorImpl.java:294 blanket IO/URL deny also hits context-provided writers and URLs Current code: OutputStream.class, Reader.class, Writer.class, Channel.class,
FileSystem.class, FileSystemProvider.class, URL.class, URI.classProblem: Deny is by supertype, so every method on any Assumption: customer VTL may call such methods. |
||
| FileSystem.class, FileSystemProvider.class, URL.class, URI.class | ||
| }; | ||
|
|
||
| /** | ||
| * The final utility holders that expose file-system operations but have no restricted | ||
| * supertype; matched by identity. | ||
| */ | ||
| private static final Class<?>[] RESTRICTED_EXACT_TYPES = { | ||
| Files.class, Paths.class | ||
| }; | ||
|
|
||
| /** | ||
| * Determines whether method execution on the given class is restricted because it belongs to | ||
| * the file, IO or network-resource type families. | ||
| * | ||
| * @param clazz class a method is about to be resolved on | ||
| * @return {@code true} if the class is a restricted resource type (deny), {@code false} otherwise | ||
| */ | ||
| private static boolean isRestrictedResourceType(final Class<?> clazz) | ||
| { | ||
| for (final Class<?> supertype : RESTRICTED_SUPERTYPES) | ||
| { | ||
| if (supertype.isAssignableFrom(clazz)) | ||
| { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| for (final Class<?> exact : RESTRICTED_EXACT_TYPES) | ||
| { | ||
| if (exact == clazz) | ||
| { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -278,3 +278,25 @@ introspector.restrict.classes = javax.management.MBeanServer | |
| introspector.restrict.classes = java.net.Socket | ||
| introspector.restrict.classes = javax.script.ScriptEngine | ||
| introspector.restrict.classes = javax.script.ScriptEngineManager | ||
|
|
||
| # File / network-resource types. These document the restricted-class intent alongside the | ||
| # entries above; note that this exact-string list cannot match concrete platform | ||
| # implementations (e.g. the JDK's internal Path type), so the hierarchy-aware check in | ||
| # SecureIntrospectorImpl is the actual enforcement floor for these families. | ||
| introspector.restrict.classes = java.net.URL | ||
| introspector.restrict.classes = java.net.URI | ||
| introspector.restrict.classes = java.nio.file.Files | ||
| introspector.restrict.classes = java.nio.file.Paths | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not go deeper here and include other packages?
Also, can we configure (env var) packages to include or exclude? that would be handy and would allow us to push the fix deeper to a more white listed stance. We update these deny lists all the time as new holes are discovered.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My opinion. Switch it to an allow list and add the default deny on the fall through.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that could break some implementations but that is where I was suggesting to move, as long as it is configurable and we log the deny message
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call — made it configurable. Pushed a follow-up that adds an operator-configurable layer on top of the code-level (type-hierarchy) floor, read via dotCMS
This gives us the deeper, whitelist-leaning stance you're after: set the restrict packages to On your point that broadening could break implementations — agreed, which is exactly why I left the defaults empty rather than denying all of One deliberate boundary: the allow-list carves classes out of the configurable layer only — it can't re-open the hardcoded floor or the existing reflection/system denials, so a config change can't accidentally undo the core fix. Easy to revisit if you'd rather it be fully override-able. Covered by a unit test (package-prefix denial + allow-list carve-out). Happy to seed a stricter default set (e.g. deny
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice. |
||
|
|
||
| # These velocity.properties lists are loaded once at engine init and matched by exact string. | ||
| # For runtime- and environment-configurable coverage that can push toward a stricter, | ||
| # package-level (prefix) stance without a code change, use the dotCMS Config properties | ||
| # (env-overridable) read by SecureIntrospectorImpl, additive to its code-level type-hierarchy | ||
| # floor: | ||
| # velocity.introspector.restrict.packages (env DOT_VELOCITY_INTROSPECTOR_RESTRICT_PACKAGES) | ||
| # comma-separated package PREFIXES to deny, e.g. "java.nio,java.net" | ||
| # velocity.introspector.restrict.classes (env DOT_VELOCITY_INTROSPECTOR_RESTRICT_CLASSES) | ||
| # comma-separated exact class names to deny | ||
| # velocity.introspector.allow.classes (env DOT_VELOCITY_INTROSPECTOR_ALLOW_CLASSES) | ||
| # comma-separated exact class names carved back out of the configurable denial | ||
| # Defaults are empty (behavior unchanged); denials are logged by the introspector. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
⚪ [P3] SecureIntrospectorImpl.java:241 swallows all Throwables when reading Config
Current code:
Problem: Catching
Throwablewithout logging silently disables the operator deny layer ifConfigmisbehaves (e.g. system-table lookup throws).Fix: