Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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)
{
/**
Expand Down Expand Up @@ -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;
}



/**
Expand Down Expand Up @@ -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)

Copy link
Copy Markdown
Contributor

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:

        catch (final Throwable t)
        {
            return EMPTY;
        }

Problem: Catching Throwable without logging silently disables the operator deny layer if Config misbehaves (e.g. system-table lookup throws).

Fix:

        catch (final Exception e)
        {
            Logger.debug(SecureIntrospectorImpl.class,
                    "Unable to read Velocity introspector config " + prop + ": " + e.getMessage());
            return EMPTY;
        }

{
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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.class

Problem: Deny is by supertype, so every method on any Writer/Reader/URL/URI reachable from the context is now inert — e.g. $response.getWriter().println(...) or $url.getPath() — not just the file/IO methods.

Assumption: customer VTL may call such methods.
What to verify: grep customer templates for getWriter, getInputStream, getPath()/getQuery() on URL/URI values; the new velocity.introspector.allow.classes is the escape hatch if so.

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
Expand Up @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not go deeper here and include other packages?

java.nio
java.net

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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 Config so it's env-overridable and updatable without a code change:

  • velocity.introspector.restrict.packages (env DOT_VELOCITY_INTROSPECTOR_RESTRICT_PACKAGES) — comma-separated package prefixes to deny, so java.nio,java.net also covers java.nio.file, java.net.http, etc.
  • velocity.introspector.restrict.classes (env DOT_VELOCITY_INTROSPECTOR_RESTRICT_CLASSES) — exact class names.
  • velocity.introspector.allow.classes (env DOT_VELOCITY_INTROSPECTOR_ALLOW_CLASSES) — carve specific classes back out of the configurable denial.

This gives us the deeper, whitelist-leaning stance you're after: set the restrict packages to java.nio,java.net (and beyond) per environment, and exempt the few classes a template legitimately needs via the allow-list. Denials are already logged in getMethod() ("... due to security restrictions").

On your point that broadening could break implementations — agreed, which is exactly why I left the defaults empty rather than denying all of java.nio/java.net out of the box: a blanket default would break common template usage (InetAddress, URLEncoder, ByteBuffer, charsets, etc.). So the aggressive stance is opt-in per environment, and the hardcoded type-hierarchy floor (File/Path/streams/URL/URI + concrete impls) stays as the always-on baseline that config can extend but not weaken.

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 java.nio/java.net with a curated allow-list) if you'd prefer that shipped on by default.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.
Loading
Loading