diff --git a/README.md b/README.md index bc1b717c..579db75c 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ Mustache.java [![Build Status](https://travis-ci.org/spullara/mustache.java.svg? [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fspullara%2Fmustache.java.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fspullara%2Fmustache.java?ref=badge_shield) ============= -Mustache.java is not designed to allow untrusted parties to provide templates. It may be possible to lock it down to provide that safely, -but by default it is UNSAFE. +**Mustache.java is not designed to allow untrusted parties to provide templates. It may be possible to lock it down to provide that safely, +but by default it is UNSAFE. Use the SafeMustacheFactory and whitelist all templates and partials.** As of release 0.9.0 mustache.java is now Java 8 only. For Java 6/7 support use 0.8.x. diff --git a/compiler/src/main/java/com/github/mustachejava/DefaultMustacheFactory.java b/compiler/src/main/java/com/github/mustachejava/DefaultMustacheFactory.java index bff6a462..481145c7 100644 --- a/compiler/src/main/java/com/github/mustachejava/DefaultMustacheFactory.java +++ b/compiler/src/main/java/com/github/mustachejava/DefaultMustacheFactory.java @@ -224,7 +224,7 @@ public int getRecursionLimit() { return recursionLimit; } - private final ThreadLocal> partialCache = ThreadLocal.withInitial(() -> new HashMap<>()); + private final ThreadLocal> partialCache = ThreadLocal.withInitial(HashMap::new); /** * In order to handle recursion, we need a temporary thread local cache during compilation diff --git a/compiler/src/main/java/com/github/mustachejava/MustacheParser.java b/compiler/src/main/java/com/github/mustachejava/MustacheParser.java index ed59dbd0..9f475c36 100644 --- a/compiler/src/main/java/com/github/mustachejava/MustacheParser.java +++ b/compiler/src/main/java/com/github/mustachejava/MustacheParser.java @@ -17,7 +17,8 @@ public class MustacheParser { public static final String DEFAULT_SM = "{{"; public static final String DEFAULT_EM = "}}"; private final boolean specConformWhitespace; - private MustacheFactory mf; + private final MustacheFactory mf; + private boolean allowChangingDelimeters = true; protected MustacheParser(MustacheFactory mf, boolean specConformWhitespace) { this.mf = mf; @@ -246,6 +247,9 @@ protected Mustache compile(final Reader reader, String tag, final AtomicInteger out = write(mv, out, file, currentLine.intValue(), startOfLine); break; case '=': + if (!allowChangingDelimeters) { + throw new MustacheException("Disallowed: changing defaul delimiters"); + } // Change delimiters out = write(mv, out, file, currentLine.intValue(), startOfLine); String trimmed = command.substring(1).trim(); @@ -314,4 +318,7 @@ private StringBuilder write(MustacheVisitor mv, StringBuilder out, String file, return new StringBuilder(); } + public void setAllowChangingDelimeters(boolean allowChangingDelimeters) { + this.allowChangingDelimeters = allowChangingDelimeters; + } } diff --git a/compiler/src/main/java/com/github/mustachejava/SafeMustacheFactory.java b/compiler/src/main/java/com/github/mustachejava/SafeMustacheFactory.java new file mode 100644 index 00000000..b22e7226 --- /dev/null +++ b/compiler/src/main/java/com/github/mustachejava/SafeMustacheFactory.java @@ -0,0 +1,91 @@ +package com.github.mustachejava; + +import com.github.mustachejava.codes.ValueCode; +import com.github.mustachejava.reflect.SimpleObjectHandler; +import com.github.mustachejava.resolver.DefaultResolver; + +import java.io.File; +import java.io.Reader; +import java.io.Writer; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Set; + +import static com.github.mustachejava.util.HtmlEscaper.escape; + +public class SafeMustacheFactory extends DefaultMustacheFactory { + + // Only allow public access + public static final SimpleObjectHandler OBJECT_HANDLER = new SimpleObjectHandler() { + @Override + protected void checkMethod(Method member) throws NoSuchMethodException { + if ((member.getModifiers() & Modifier.PUBLIC) != Modifier.PUBLIC) { + throw new NoSuchMethodException("Only public members allowed"); + } + } + + @Override + protected void checkField(Field member) throws NoSuchFieldException { + if ((member.getModifiers() & Modifier.PUBLIC) != Modifier.PUBLIC) { + throw new NoSuchFieldException("Only public members allowed"); + } + } + }; + + public SafeMustacheFactory(Set allowedResourceNames, String resourceRoot) { + super(new DefaultResolver(resourceRoot) { + @Override + public Reader getReader(String resourceName) { + // Only allow allowed resources + if (allowedResourceNames.contains(resourceName)) { + return super.getReader(resourceName); + } + throw new MustacheException("Disallowed: resource requested"); + } + }); + setup(); + } + + public SafeMustacheFactory(Set allowedResourceNames, File fileRoot) { + super(new DefaultResolver(fileRoot) { + @Override + public Reader getReader(String resourceName) { + // Only allow allowed resources + if (allowedResourceNames.contains(resourceName)) { + return super.getReader(resourceName); + } + throw new MustacheException("Disallowed: resource requested"); + } + }); + setup(); + } + + private void setup() { + setObjectHandler(OBJECT_HANDLER); + mc.setAllowChangingDelimeters(false); + } + + @Override + public MustacheVisitor createMustacheVisitor() { + return new DefaultMustacheVisitor(this) { + @Override + public void pragma(TemplateContext tc, String pragma, String args) { + throw new MustacheException("Disallowed: pragmas in templates"); + } + + @Override + public void value(TemplateContext tc, String variable, boolean encoded) { + if (!encoded) { + throw new MustacheException("Disallowed: non-encoded text in templates"); + } + list.add(new ValueCode(tc, df, variable, encoded)); + } + }; + } + + @Override + public void encode(String value, Writer writer) { + escape(value, writer); + } +} diff --git a/compiler/src/main/java/com/github/mustachejava/reflect/BaseObjectHandler.java b/compiler/src/main/java/com/github/mustachejava/reflect/BaseObjectHandler.java index 56c20811..35f14282 100644 --- a/compiler/src/main/java/com/github/mustachejava/reflect/BaseObjectHandler.java +++ b/compiler/src/main/java/com/github/mustachejava/reflect/BaseObjectHandler.java @@ -193,7 +193,7 @@ protected void checkField(Field member) throws NoSuchFieldException { } // We default to not allowing private classes - private boolean checkClass(Class sClass) { + protected boolean checkClass(Class sClass) { return (sClass.getModifiers() & Modifier.PUBLIC) != Modifier.PUBLIC; } diff --git a/compiler/src/test/java/com/github/mustachejava/InterpreterTest.java b/compiler/src/test/java/com/github/mustachejava/InterpreterTest.java index cdbf8c3c..f7370c89 100644 --- a/compiler/src/test/java/com/github/mustachejava/InterpreterTest.java +++ b/compiler/src/test/java/com/github/mustachejava/InterpreterTest.java @@ -56,6 +56,24 @@ int taxed_value() { assertEquals(getContents(root, "simple.txt"), sw.toString()); } + public void testSafeSimple() throws MustacheException, IOException, ExecutionException, InterruptedException { + MustacheFactory c = new SafeMustacheFactory(Collections.singleton("simple.html"), root); + Mustache m = c.compile("simple.html"); + StringWriter sw = new StringWriter(); + m.execute(sw, new Object() { + public String name = "Chris"; + public int value = 10000; + + public int taxed_value() { + return (int) (this.value - (this.value * 0.4)); + } + + public boolean in_ca = true; + }); + assertEquals(getContents(root, "simple.txt"), sw.toString()); + } + + private static class LocalizedMustacheResolver extends DefaultResolver { private final Locale locale;