mraible commented on code in PR #169:
URL: https://github.com/apache/roller/pull/169#discussion_r3891439156
##########
app/src/main/webapp/themes/frontpage/directory.vm:
##########
@@ -31,10 +31,15 @@
<div id="tabContent">
<div id="directory">
- #if($model.getRequestParameter("weblog"))
- #set($handle = $model.getRequestParameter("weblog"))
- <a href="?letter=$utils.left($handle,1)">Back to blog
directory</a>
- #set($profileWeblog = $site.getWeblog($handle))
+ ## Render the profile only for a weblog that exists, and build
+ ## the back-link from the resolved weblog's own handle.
+ #set($profileWeblog = false)
+ #set($requestedHandle = $model.getRequestParameter("weblog"))
+ #if($requestedHandle)
+ #set($profileWeblog = $site.getWeblog($requestedHandle))
Review Comment:
`$site.getWeblog()` still receives the raw parameter.
`JPAWeblogManagerImpl.getWeblogByHandle` throws `WebloggerException("Invalid
handle: '...'")` for anything outside `[A-Za-z0-9_]`, and `SiteModel.getWeblog`
logs that at ERROR with a stack trace, so an anonymous loop over
`/page/directory?weblog=<junk>` fills the log with attacker-controlled text
(CR/LF included). A cheap pre-check such as `#if($requestedHandle &&
$requestedHandle.matches("[A-Za-z0-9_]+"))` keeps garbage from reaching the
manager.
##########
app/src/main/webapp/themes/frontpage/directory.vm:
##########
@@ -31,10 +31,15 @@
<div id="tabContent">
<div id="directory">
- #if($model.getRequestParameter("weblog"))
- #set($handle = $model.getRequestParameter("weblog"))
- <a href="?letter=$utils.left($handle,1)">Back to blog
directory</a>
- #set($profileWeblog = $site.getWeblog($handle))
+ ## Render the profile only for a weblog that exists, and build
+ ## the back-link from the resolved weblog's own handle.
+ #set($profileWeblog = false)
+ #set($requestedHandle = $model.getRequestParameter("weblog"))
+ #if($requestedHandle)
+ #set($profileWeblog = $site.getWeblog($requestedHandle))
+ #end
+ #if($profileWeblog)
Review Comment:
Handles may start with a digit (`username.allowedChars` defaults to
`A-Za-z0-9`), and `getWeblogsByLetter` handled `?letter=2` before. Now `2`
isn't a key in the A-Z map, so "Back to blog directory" from such a profile
lands on the unfiltered list. Either accept the resolved handle's first
character in `_blogdirectory.vm` (it's trusted) or omit the `letter` param when
it isn't A-Z.
##########
app/src/main/webapp/themes/frontpage/_blogdirectory.vm:
##########
@@ -1,8 +1,14 @@
-#if($model.getRequestParameter("letter"))
- #set($chosenLetter = $model.getRequestParameter("letter"))
- #end
+#set($weblogLetterMap = $site.getWeblogHandleLetterMap())
- #set($weblogLetterMap = $site.getWeblogHandleLetterMap())
+ ## Accept only a known A-Z key; otherwise render the full listing, exactly
+ ## as a missing parameter does.
+ #set($requestedLetter = $model.getRequestParameter("letter"))
+ #if($requestedLetter && $requestedLetter.length() == 1)
+ #set($candidateLetter = $requestedLetter.toUpperCase())
Review Comment:
Minor: `toUpperCase()` uses the JVM default locale, so on a Turkish-locale
server `i` becomes `İ` and `?letter=i` is rejected while `?letter=I` works. A
`Locale.ROOT` upper-case helper on `$utils` would avoid it.
##########
app/src/main/webapp/themes/frontpage/directory.vm:
##########
@@ -31,10 +31,15 @@
<div id="tabContent">
<div id="directory">
- #if($model.getRequestParameter("weblog"))
- #set($handle = $model.getRequestParameter("weblog"))
- <a href="?letter=$utils.left($handle,1)">Back to blog
directory</a>
- #set($profileWeblog = $site.getWeblog($handle))
+ ## Render the profile only for a weblog that exists, and build
+ ## the back-link from the resolved weblog's own handle.
+ #set($profileWeblog = false)
Review Comment:
Nit: nothing sets `$profileWeblog` before this and Velocity 2.4 assigns null
from `#set`, so the initialiser is a no-op. If it's kept for the ROL-689
precedent in `weblog.vm`, a comment saying so would help.
##########
app/src/main/webapp/themes/frontpage/directory.vm:
##########
@@ -31,10 +31,15 @@
<div id="tabContent">
<div id="directory">
- #if($model.getRequestParameter("weblog"))
- #set($handle = $model.getRequestParameter("weblog"))
- <a href="?letter=$utils.left($handle,1)">Back to blog
directory</a>
- #set($profileWeblog = $site.getWeblog($handle))
+ ## Render the profile only for a weblog that exists, and build
+ ## the back-link from the resolved weblog's own handle.
+ #set($profileWeblog = false)
+ #set($requestedHandle = $model.getRequestParameter("weblog"))
+ #if($requestedHandle)
+ #set($profileWeblog = $site.getWeblog($requestedHandle))
+ #end
+ #if($profileWeblog)
+ <a
href="?letter=$utils.escapeHTML($utils.left($profileWeblog.handle,1))">Back to
blog directory</a>
Review Comment:
Nit: `escapeHTML` can't change anything here, `$profileWeblog.handle`
already passed the `[A-Za-z0-9_]` check in `getWeblogByHandle`.
##########
app/src/main/webapp/themes/frontpage/_blogdirectory.vm:
##########
@@ -1,8 +1,14 @@
-#if($model.getRequestParameter("letter"))
- #set($chosenLetter = $model.getRequestParameter("letter"))
- #end
+#set($weblogLetterMap = $site.getWeblogHandleLetterMap())
- #set($weblogLetterMap = $site.getWeblogHandleLetterMap())
+ ## Accept only a known A-Z key; otherwise render the full listing, exactly
+ ## as a missing parameter does.
+ #set($requestedLetter = $model.getRequestParameter("letter"))
+ #if($requestedLetter && $requestedLetter.length() == 1)
Review Comment:
Nit: the `length() == 1` check and `$candidateLetter` collapse to
`#if($requestedLetter &&
$weblogLetterMap.containsKey($requestedLetter.toUpperCase()))`, since every key
is one character. Keep the null guard, `TreeMap.containsKey(null)` throws.
##########
app/src/test/java/org/apache/roller/weblogger/ui/rendering/velocity/FrontpageDirectoryRenderingTest.java:
##########
@@ -0,0 +1,209 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. The ASF licenses this file to You
+ * under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the License for the specific language governing
+ * permissions and limitations under the License. For additional
+ * information regarding copyright in this work, please see the NOTICE
+ * file in the top level directory of this distribution.
+ */
+package org.apache.roller.weblogger.ui.rendering.velocity;
+
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.VelocityEngine;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Renders the bundled frontpage blog-directory template against the real
+ * Velocity engine and asserts how it treats the caller-supplied
+ * <code>letter</code> parameter.
+ *
+ * <p>The template is reached anonymously, so the parameter is untrusted. The
+ * contract is that only a value which normalizes to one of the directory's own
+ * A-Z keys is used, and that anything else falls back to the complete
directory
+ * without the rejected value appearing in the response in any form — raw,
+ * HTML-encoded, or URL-encoded.
+ */
+public class FrontpageDirectoryRenderingTest {
+
+ private static final String THEME_DIR = "src/main/webapp/themes/frontpage";
+ private static final String TEMPLATE = "_blogdirectory.vm";
+
+ private static VelocityEngine engine;
+
+ @BeforeAll
+ public static void setUpEngine() {
+ Properties props = new Properties();
+ props.setProperty("resource.loaders", "file");
+ props.setProperty("resource.loader.file.class",
+
"org.apache.velocity.runtime.resource.loader.FileResourceLoader");
+ props.setProperty("resource.loader.file.path", THEME_DIR);
+ engine = new VelocityEngine();
+ engine.init(props);
+ }
+
+ /** Minimal stand-ins for the model objects the template reads. */
+ public static class StubModel {
+ private final String letter;
+ StubModel(String letter) { this.letter = letter; }
+ public String getRequestParameter(String name) {
+ return "letter".equals(name) ? letter : null;
+ }
+ }
+
+ public static class StubPager {
+ public List<Object> getItems() { return new ArrayList<>(); }
+ public String prevLink() { return null; }
+ public String nextLink() { return null; }
+ public String prevName() { return null; }
+ public String nextName() { return null; }
+ }
+
+ public static class StubSite {
+ public Map<String, Long> getWeblogHandleLetterMap() {
+ Map<String, Long> map = new LinkedHashMap<>();
+ for (char c = 'A'; c <= 'Z'; c++) {
+ map.put(String.valueOf(c), 1L);
+ }
+ return map;
+ }
+ public StubPager getWeblogsByLetterPager(String letter, int offset,
int length) {
+ return new StubPager();
+ }
+ }
+
+ public static class StubUtils {
Review Comment:
Nit: `UtilitiesModel` has a no-arg constructor and `left` / `escapeHTML` are
stateless, so `ctx.put("utils", new UtilitiesModel())` tests the real
`escapeHtml4` instead of a four-replace stand-in.
##########
app/src/test/java/org/apache/roller/weblogger/ui/rendering/velocity/FrontpageDirectoryRenderingTest.java:
##########
@@ -0,0 +1,209 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. The ASF licenses this file to You
+ * under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the License for the specific language governing
+ * permissions and limitations under the License. For additional
+ * information regarding copyright in this work, please see the NOTICE
+ * file in the top level directory of this distribution.
+ */
+package org.apache.roller.weblogger.ui.rendering.velocity;
+
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.VelocityEngine;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Renders the bundled frontpage blog-directory template against the real
+ * Velocity engine and asserts how it treats the caller-supplied
+ * <code>letter</code> parameter.
+ *
+ * <p>The template is reached anonymously, so the parameter is untrusted. The
+ * contract is that only a value which normalizes to one of the directory's own
+ * A-Z keys is used, and that anything else falls back to the complete
directory
+ * without the rejected value appearing in the response in any form — raw,
+ * HTML-encoded, or URL-encoded.
+ */
+public class FrontpageDirectoryRenderingTest {
+
+ private static final String THEME_DIR = "src/main/webapp/themes/frontpage";
+ private static final String TEMPLATE = "_blogdirectory.vm";
+
+ private static VelocityEngine engine;
+
+ @BeforeAll
+ public static void setUpEngine() {
+ Properties props = new Properties();
+ props.setProperty("resource.loaders", "file");
+ props.setProperty("resource.loader.file.class",
+
"org.apache.velocity.runtime.resource.loader.FileResourceLoader");
+ props.setProperty("resource.loader.file.path", THEME_DIR);
Review Comment:
`app/pom.xml` already copies `src/main/webapp/themes/**` onto the test
classpath (that's how `themes.dir` in `roller-custom.properties` works), so a
`ClasspathResourceLoader` rooted at `themes/frontpage/` drops the
working-directory dependence.
##########
app/src/test/java/org/apache/roller/weblogger/ui/rendering/velocity/FrontpageDirectoryRenderingTest.java:
##########
@@ -0,0 +1,209 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. The ASF licenses this file to You
+ * under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the License for the specific language governing
+ * permissions and limitations under the License. For additional
+ * information regarding copyright in this work, please see the NOTICE
+ * file in the top level directory of this distribution.
+ */
+package org.apache.roller.weblogger.ui.rendering.velocity;
+
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.VelocityEngine;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Renders the bundled frontpage blog-directory template against the real
+ * Velocity engine and asserts how it treats the caller-supplied
+ * <code>letter</code> parameter.
+ *
+ * <p>The template is reached anonymously, so the parameter is untrusted. The
+ * contract is that only a value which normalizes to one of the directory's own
+ * A-Z keys is used, and that anything else falls back to the complete
directory
+ * without the rejected value appearing in the response in any form — raw,
+ * HTML-encoded, or URL-encoded.
+ */
+public class FrontpageDirectoryRenderingTest {
+
+ private static final String THEME_DIR = "src/main/webapp/themes/frontpage";
+ private static final String TEMPLATE = "_blogdirectory.vm";
+
+ private static VelocityEngine engine;
+
+ @BeforeAll
+ public static void setUpEngine() {
+ Properties props = new Properties();
+ props.setProperty("resource.loaders", "file");
+ props.setProperty("resource.loader.file.class",
+
"org.apache.velocity.runtime.resource.loader.FileResourceLoader");
+ props.setProperty("resource.loader.file.path", THEME_DIR);
+ engine = new VelocityEngine();
+ engine.init(props);
+ }
+
+ /** Minimal stand-ins for the model objects the template reads. */
+ public static class StubModel {
+ private final String letter;
+ StubModel(String letter) { this.letter = letter; }
+ public String getRequestParameter(String name) {
+ return "letter".equals(name) ? letter : null;
+ }
+ }
+
+ public static class StubPager {
+ public List<Object> getItems() { return new ArrayList<>(); }
+ public String prevLink() { return null; }
+ public String nextLink() { return null; }
+ public String prevName() { return null; }
+ public String nextName() { return null; }
+ }
+
+ public static class StubSite {
+ public Map<String, Long> getWeblogHandleLetterMap() {
+ Map<String, Long> map = new LinkedHashMap<>();
+ for (char c = 'A'; c <= 'Z'; c++) {
+ map.put(String.valueOf(c), 1L);
+ }
+ return map;
+ }
+ public StubPager getWeblogsByLetterPager(String letter, int offset,
int length) {
+ return new StubPager();
+ }
+ }
+
+ public static class StubUtils {
+ public String escapeHTML(String str) {
+ return str == null ? null : str.replace("&", "&").replace("<",
"<")
+ .replace(">", ">").replace("\"", """);
+ }
+ public String left(String str, int len) {
+ if (str == null) { return null; }
+ return str.length() <= len ? str : str.substring(0, len);
+ }
+ }
+
+ public static class StubUrl {
+ public String getAbsoluteSite() { return "http://example.test"; }
+ }
+
+ private String render(String letterParam) throws Exception {
+ VelocityContext ctx = new VelocityContext();
+ ctx.put("model", new StubModel(letterParam));
+ ctx.put("site", new StubSite());
+ ctx.put("utils", new StubUtils());
+ ctx.put("url", new StubUrl());
+ ctx.put("pageLength", 30);
+ StringWriter out = new StringWriter();
+ engine.mergeTemplate(TEMPLATE, "UTF-8", ctx, out);
+ return out.toString();
+ }
+
+ @Test
+ public void missingLetterRendersCompleteDirectory() throws Exception {
+ String html = render(null);
+ assertTrue(html.contains("All weblogs"),
+ "a missing letter must render the complete directory:\n" +
html);
+ assertFalse(html.contains("Weblogs starting with"),
+ "a missing letter must not render a filtered heading");
+ }
+
+ @Test
+ public void validUppercaseLetterIsAccepted() throws Exception {
+ String html = render("A");
+ assertTrue(html.contains("Weblogs starting with A"),
+ "a valid key must be accepted:\n" + html);
+ }
+
+ @Test
+ public void lowercaseLetterNormalizesToTheSameGroup() throws Exception {
+ assertTrue(render("a").contains("Weblogs starting with A"),
+ "lowercase input must normalize to the uppercase key");
+ }
+
+ /**
+ * Every value that is not a single A-Z key must be discarded outright and
+ * must not be echoed, raw or encoded.
+ */
+ @Test
+ public void invalidValuesFallBackAndAreNotEchoed() throws Exception {
+ String[] rejected = {
+ "AB", // multi-character
+ "1", // numeric
+ "!", // punctuation
+ "é", // non-ASCII
+ "<script>alert(1)</script>", // script payload
+ "\" onmouseover=\"alert(1)", // attribute-breaking payload
+ "A<b>", // valid prefix, invalid remainder
+ };
+ for (String value : rejected) {
+ String html = render(value);
+ assertTrue(html.contains("All weblogs"),
+ "rejected value [" + value + "] must fall back to the
complete "
+ + "directory:\n" + html);
+ // Assert against the heading directly. A bare contains(value)
would
+ // match incidentally: single characters such as "1" occur
naturally
+ // in the rendered letter counts.
+ assertFalse(html.contains("Weblogs starting with"),
+ "rejected value [" + value + "] produced a filtered
heading:\n" + html);
+ assertFalse(html.contains("<script") ||
html.contains("<script"),
+ "rejected value [" + value + "] reached the page, raw or
encoded:\n" + html);
+ assertFalse(html.contains("onmouseover"),
+ "rejected value [" + value + "] leaked an event
handler:\n" + html);
+ }
+ }
+
+ /**
+ * The sibling directory template resolves a weblog handle from the query
+ * string. It cannot be rendered standalone here because it pulls in other
+ * templates through #includeTemplate, so this is a structural check: the
+ * link must be built from the resolved weblog rather than the raw
+ * parameter, and escaped at output.
+ */
+ @Test
+ public void directoryTemplateValidatesTheWeblogParameter() throws
Exception {
Review Comment:
This asserts source substrings of `directory.vm`, so a whitespace change
breaks it while a re-introduced raw parameter under another variable name
passes. `#includeTemplate` is a plain velocimacro in
`WEB-INF/velocity/weblog.vm`, so the test can stub it inline
(`#macro(includeTemplate $w $p)#end` before `#parse('directory.vm')`) and
assert the rendered output: no profile for an unknown handle, back-link built
from the resolved handle for a known one.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]