Skip to content

Resolve weblog template resources within the active theme - #172

Open
snoopdave wants to merge 4 commits into
masterfrom
weblog-template-resource-loading
Open

Resolve weblog template resources within the active theme#172
snoopdave wants to merge 4 commits into
masterfrom
weblog-template-resource-loading

Conversation

@snoopdave

Copy link
Copy Markdown
Contributor

Weblog template rendering currently loads resources from the application
classpath in addition to the theme namespace. This change confines template
resource resolution to the active theme and restricts include targets to the
shapes a template actually takes.

What changed

  • Drop the classpath resource loader from the loader set used for weblog
    rendering, and align the stale test copy of velocity.properties.
  • Register an include event handler that confines #include / #parse to the
    theme namespace and refuses absolute paths, upward traversal, schemes, and
    names that are neither a stored template id nor a .vm file.
  • Keep SecureUberspector unchanged.

Tests

ThemeIncludeConfinementTest covers loader set, handler registration, and
uberspector configuration; handler unit cases for refused and allowed names; and
an end-to-end engine test that measures each control independently.

Weblog templates are authored by weblog administrators, whom Roller already
treats as untrusted: the rendering engine runs them under SecureUberspector.
That sandbox governs method access rather than resource resolution, so the
loader set and the include directives are constrained to match it.

The loader set for weblog rendering is now the webapp templates, the active
theme, and the weblog's own stored templates. A ThemeIncludeEventHandler keeps
#include and #parse within the namespace they are written in, refusing names
that are absolute, walk upward, or carry a scheme.

Macro libraries and feed templates resolve through the webapp loader and are
unaffected.
The copy under src/test/resources had drifted: it carried a loader set the
webapp no longer uses and lacked the introspection sandbox entirely. Nothing
reads it today — the servlet context resolves /WEB-INF/velocity.properties
from src/main/webapp — but a second configuration that disagrees with the
shipped one is a configuration that can quietly become live.

The configuration assertions now run over both files so they cannot drift
apart again.
The include directives resolve names through whichever loaders are configured,
so a name needs no traversal to reach whatever those loaders can see. Constrain
the names themselves as well as the loader set: a stored template id carries no
extension, and a theme resource is a Velocity template, so a name bearing some
other extension is not a template reference and is refused.

The engine test now measures the loader set and the include handler separately,
against a reference rendering, so neither is resting on the other.
The engine test now demonstrates the classpath loader was reachable and is now
closed against a generic test-fixture property rather than a named packaged
resource, and the refused-name list uses generic paths.

Claude-Session: https://claude.ai/code/session_01A1fhY1E2PCFU6UAPXu2WtV

@mraible mraible left a comment

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.

Dropping the classpath loader and confining #include / #parse is the right fix, but the handler as written refuses Roller's own template ids, so every weblog on a shared (bundled) theme renders without its entries, sidebar, header and footer. That's a blocker; details inline.

Shared-theme template ids are <themeId>:<templateName> (SharedThemeFromDir lines 270 and 368), and the includeTemplate macro in WEB-INF/velocity/weblog.vm calls #parse($pageArg.id + '|' + $model.deviceType), i.e. basic:_day|standard. isOutsideNamespace sees a colon with no slash and returns null, which Velocity treats as "render nothing", with a WARN per include per page view. Only custom-theme weblogs (UUID template ids) keep working, which is also why the new test suite stays green: its allowed list never includes the theme:name|device shape.

Suggested fix: treat <something>:<name> as the theme namespace when the part before the colon has no /, \ or . in it (a scheme like file: / http: / jar: is followed by // or a path, and a Windows drive is a single letter), and drop the extension check entirely, since template names are free-form (basic-custom.css is a template in the basic theme). Then add basic:_day|standard and basic:basic-custom.css|standard to legitimateIncludesStillPass.

int colon = normalized.indexOf(':');
if (colon > -1) {
int slash = normalized.indexOf('/');
return slash == -1 || colon < slash;

@mraible mraible Aug 31, 2026

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.

This refuses every shared-theme template id. SharedThemeFromDir builds ids as themeId + ":" + templateName (lines 270, 368) and weblog.vm's includeTemplate macro does #parse($pageArg.id + '|' + $model.deviceType), so the basic theme's weblog page asks for basic:_day|standard, gets null back, and renders nothing for that fragment. Every bundled theme is affected; only custom (UUID-id) templates pass. A scheme is followed by / (file:///, http://, jar:file:) or is a single drive letter, so something like colon > 1 && (slash == -1 || colon < slash) && normalized.charAt(colon + 1) != '/' is closer, and ThemeResourceLoader is the real authority on what a theme id looks like.

// No extension: a stored template id.
return false;
}
return !name.regionMatches(true, dot, ".vm", 0, 3) || dot != name.length() - 3;

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.

Shared-theme template names are free-form: the basic theme's stylesheet template is literally named basic-custom.css, and the template guide documents inlining it through #includeTemplate. This check strips |standard, sees .css, and refuses it. Since the resource loaders are already confined to the theme and webapp namespaces, I'd drop the extension check rather than try to enumerate template-name shapes.

@Test
public void legitimateIncludesStillPass() {
ThemeIncludeEventHandler handler = new ThemeIncludeEventHandler();
String[] allowed = {

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.

None of these are the shape Roller's own macros pass to #parse. Add basic:_day|standard and basic:basic-custom.css|standard here; both fail against the current handler and would have caught the regression.

// Logged rather than raised: a template that asks for something it
// may not have renders without that fragment, which is how Velocity
// already treats a resource it cannot find.
LOG.warn("Refusing #" + directiveName + " of '" + path

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.

Once the id bug is fixed this is fine, but note it's unthrottled: any weblog admin can make the server log a WARN per request by leaving a refused include in a public template. Debug, or a once-per-template warning, would be safer.

# loader set is limited to the webapp templates, the active theme, and the
# weblog's own stored templates. The classpath is deliberately not a
# resolvable namespace for them.
resource.loaders = webapp, theme, roller

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.

With the class loader gone, RollerVelocity (lines 66-67) still sets resource.loader.class.cache and resource.loader.class.modification_check_interval under themes.reload.mode; those two lines should go in the same change.


# Weblog templates render under SecureUberspector, which governs method access
# rather than resource resolution, so the include directives are confined
# separately. Keep this aligned with /WEB-INF/velocity.properties.

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.

The commit message says nothing reads this copy. Rather than keep two hand-maintained copies of security-relevant config plus a test that scans both, I'd delete this one.


private String read(Path path) throws Exception {
assertTrue(Files.isReadable(path),
"cannot read " + path.toAbsolutePath() + " (run from the app module)");

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.

cwd-relative, so this only runs from app/; surefire sets project.build.directory for this module (see ApplicationResourcesTest), which would let it run from an IDE rooted at the repo.

*/
@Test
public void aPlainNameDoesNotReachAPackagedFile() throws Exception {
Path dir = Files.createTempDirectory("roller-include-confinement");

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.

Nit: the temp directory is never deleted; a @TempDir parameter does the cleanup.

return null;
}

String path = includeResourcePath.trim();

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.

Nit: trim() is computed twice, and returning the trimmed value means #parse(" $pageId") now resolves a different name than before. Compute it once and return the original unless trimming is intentional.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants