diff --git a/CHANGES.md b/CHANGES.md index ae054e20d7..f85731857a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,14 @@ # Apache Roller — Changes +## 6.1.8 (in development) + +### Bug fixes + +- **Meta descriptions are always encoded.** A weblog's "about" text or an + entry's search description containing a quote no longer cuts the + `` tag short, whether or not weblog admins are + trusted. + ## 6.1.7 ### Improvements diff --git a/app/src/main/webapp/WEB-INF/velocity/weblog.vm b/app/src/main/webapp/WEB-INF/velocity/weblog.vm index 8ff1065152..58da142ae1 100644 --- a/app/src/main/webapp/WEB-INF/velocity/weblog.vm +++ b/app/src/main/webapp/WEB-INF/velocity/weblog.vm @@ -128,12 +128,13 @@ blog entry (permalink) page. If the relevant description value has not been con will be created. *# #macro(showMetaDescription) + ## escaped once whatever the trust setting: tags removed, entities decoded, then encoded #if ($model.permalink) #if ($utils.isNotEmpty($model.weblogEntry.searchDescription)) - + #end #elseif ($utils.isNotEmpty($model.weblog.about)) - + #end #end diff --git a/app/src/main/webapp/themes/gaurav/std_head.vm b/app/src/main/webapp/themes/gaurav/std_head.vm index e68bba6e7b..ec5bf81e8e 100755 --- a/app/src/main/webapp/themes/gaurav/std_head.vm +++ b/app/src/main/webapp/themes/gaurav/std_head.vm @@ -1,9 +1,9 @@ #if ($model.permalink == false) - + #else - + #end diff --git a/app/src/test/java/org/apache/roller/weblogger/ui/rendering/velocity/MetaDescriptionEscapingTest.java b/app/src/test/java/org/apache/roller/weblogger/ui/rendering/velocity/MetaDescriptionEscapingTest.java new file mode 100644 index 0000000000..38d508d2a0 --- /dev/null +++ b/app/src/test/java/org/apache/roller/weblogger/ui/rendering/velocity/MetaDescriptionEscapingTest.java @@ -0,0 +1,137 @@ +/* + * 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.util.Properties; + +import org.apache.roller.weblogger.ui.rendering.model.UtilitiesModel; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.VelocityEngine; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The weblog's "about" text and an entry's search description are written into + * a {@code } attribute. They are stored as typed when weblog + * admins are trusted, and HTML-encoded on output when they aren't, so the + * templates must encode them exactly once: a quote must not end the attribute, + * and text the sanitizer already encoded must not be encoded twice. + */ +public class MetaDescriptionEscapingTest { + + /** Minimal stand-ins for the model objects the templates read. */ + public static class StubModel { + private final boolean permalink; + private final StubWeblog weblog; + private final StubEntry entry; + + StubModel(boolean permalink, String text) { + this.permalink = permalink; + this.weblog = new StubWeblog(text); + this.entry = new StubEntry(text); + } + + public boolean isPermalink() { + return permalink; + } + + public StubWeblog getWeblog() { + return weblog; + } + + public StubEntry getWeblogEntry() { + return entry; + } + } + + public static class StubWeblog { + private final String about; + + StubWeblog(String about) { + this.about = about; + } + + public String getAbout() { + return about; + } + } + + public static class StubEntry { + private final String searchDescription; + + StubEntry(String searchDescription) { + this.searchDescription = searchDescription; + } + + public String getSearchDescription() { + return searchDescription; + } + } + + private static String render(String loaderPath, String source, boolean permalink, String text) { + 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", loaderPath); + VelocityEngine engine = new VelocityEngine(); + engine.init(props); + + VelocityContext ctx = new VelocityContext(); + ctx.put("model", new StubModel(permalink, text)); + ctx.put("utils", new UtilitiesModel()); + StringWriter out = new StringWriter(); + engine.evaluate(ctx, out, "test", source); + return out.toString(); + } + + private static String sharedMacro(boolean permalink, String text) { + return render("src/main/webapp/WEB-INF/velocity", + "#parse('weblog.vm')#showMetaDescription()", permalink, text); + } + + private static String gaurav(boolean permalink, String text) { + return render("src/main/webapp/themes/gaurav", "#parse('std_head.vm')", permalink, text); + } + + /** Stored value, and what the attribute must contain. */ + private static final String[][] CASES = { + // stored as typed (trusted admins): a quote must not close the attribute + {"Tips \" autofocus onfocus=\"alert(1)", "Tips " autofocus onfocus="alert(1)"}, + // already encoded by the sanitizer (untrusted admins): encoded once, not twice + {"Tips " autofocus onfocus="alert(1)", "Tips " autofocus onfocus="alert(1)"}, + {"Matt's \"weblog\" & notes", "Matt's "weblog" & notes"}, + {"Bold text", "Bold text"}, + }; + + @Test + public void metaDescriptionIsEncodedOnce() { + for (String[] c : CASES) { + String[] outputs = { + sharedMacro(false, c[0]), sharedMacro(true, c[0]), + gaurav(false, c[0]), gaurav(true, c[0]) + }; + for (String out : outputs) { + assertTrue(out.contains("content=\"" + c[1] + "\">"), c[0] + " gave: " + out); + } + } + } +}