Skip to content

Commit

Permalink
Add setting to limit the size of output when evaluating a template (P…
Browse files Browse the repository at this point in the history
…ebbleTemplates#529)

* Add config to limit the size of the rendered output

* Add docs about size limiting

* Add doc

Co-authored-by: Victor Grigoriu <grigoriu@adobe.com>
Co-authored-by: Eric Bussieres <erbussieres@gmail.com>
  • Loading branch information
3 people authored Jul 31, 2020
1 parent 821e229 commit 4d27af7
Show file tree
Hide file tree
Showing 14 changed files with 325 additions and 13 deletions.
19 changes: 17 additions & 2 deletions docs/src/orchid/resources/wiki/guide/basic-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ Tests can be negated by using the is not operator:
```

### Conditional (Ternary) Operator
The conditional operator is similar to it's Java counterpart:
The conditional operator is similar to its Java counterpart:
```twig
{% verbatim %}{{ foo ? "yes" : "no" }}{% endverbatim %}
```
Expand All @@ -412,5 +412,20 @@ In order from highest to lowest precedence:
- `and`
- `or`

### Limiting the size of the rendered output

In case you’re running Pebble with templates provided by someone else, there’s an attack similar to
[zip bombs](https://en.wikipedia.org/wiki/Zip_bomb) or [XML bombs](https://en.wikipedia.org/wiki/Billion_laughs_attack)
that might cause your process to run out of memory. To protect against it, you can limit the size of the output when
evaluating a template:
```java
PebbleEngine pebble = new PebbleEngine.Builder()
// Output should not exceed 10 MB.
.maxRenderedSize(10 * 1024 * 1024)
.build();
```

This will throw a `PebbleException` when a template evaluation tries to write more characters than the limit you set.

### IDE's plugin
If you want to add IDE's syntax highlighting, you can install this [plugin](https://plugins.jetbrains.com/idea/plugin/9407-pebble) for IntelliJ. Thank you to Bastien Jansen for his contribution.
If you want to add IDE's syntax highlighting, you can install this [plugin](https://plugins.jetbrains.com/idea/plugin/9407-pebble) for IntelliJ. Thank you to Bastien Jansen for his contribution.
1 change: 1 addition & 0 deletions docs/src/orchid/resources/wiki/guide/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,4 @@ All the settings are set during the construction of the `PebbleEngine` object.
| `literalDecimalTreatedAsInteger` | option for toggling to enable/disable literal decimal treated as integer | `false` |
| `literalNumbersAsBigDecimals` | option for toggling to enable/disable literal numbers treated as BigDecimals | `false` |
| `greedyMatchMethod` | option for toggling to enable/disable greedy matching mode for finding java method. Reduce the limit of the parameter type, try to find other method which has compatible parameter types. | `false` |
| `maxRenderedSize` | option for limiting the size of the rendered output | `-1 (disabled)` |
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ public class PebbleEngine {

private final Locale defaultLocale;

private final int maxRenderedSize;

private final PebbleCache<CacheKey, Object> tagCache;

private final ExecutorService executorService;
Expand All @@ -92,6 +94,7 @@ private PebbleEngine(Loader<?> loader,
Syntax syntax,
boolean strictVariables,
Locale defaultLocale,
int maxRenderedSize,
PebbleCache<CacheKey, Object> tagCache,
PebbleCache<Object, PebbleTemplate> templateCache,
ExecutorService executorService,
Expand All @@ -103,6 +106,7 @@ private PebbleEngine(Loader<?> loader,
this.syntax = syntax;
this.strictVariables = strictVariables;
this.defaultLocale = defaultLocale;
this.maxRenderedSize = maxRenderedSize;
this.tagCache = tagCache;
this.executorService = executorService;
this.templateCache = templateCache;
Expand Down Expand Up @@ -221,6 +225,15 @@ public Locale getDefaultLocale() {
return this.defaultLocale;
}

/**
* Returns the max rendered size.
*
* @return The max rendered size.
*/
public int getMaxRenderedSize() {
return this.maxRenderedSize;
}

/**
* Returns the executor service
*
Expand Down Expand Up @@ -274,6 +287,8 @@ public static class Builder {

private Locale defaultLocale;

private int maxRenderedSize = -1;

private ExecutorService executorService;

private PebbleCache<Object, PebbleTemplate> templateCache;
Expand Down Expand Up @@ -386,6 +401,19 @@ public Builder defaultLocale(Locale defaultLocale) {
return this;
}

/**
* Sets the maximum size of the rendered template to protect against macro bombs.
* See for example https://github.com/PebbleTemplates/pebble/issues/526.
* If the rendered template exceeds this limit, then a PebbleException is thrown.
* The default value is -1 and it means unlimited.
* @param maxRenderedSize The maximum allowed size of the rendered template.
* @return This builder object.
*/
public Builder maxRenderedSize(int maxRenderedSize) {
this.maxRenderedSize = maxRenderedSize;
return this;
}

/**
* Sets the executor service which is required if using one of Pebble's multithreading features
* such as the "parallel" tag.
Expand Down Expand Up @@ -585,7 +613,7 @@ public PebbleEngine build() {

EvaluationOptions evaluationOptions = new EvaluationOptions(this.greedyMatchMethod,
this.methodAccessValidator);
return new PebbleEngine(this.loader, this.syntax, this.strictVariables, this.defaultLocale,
return new PebbleEngine(this.loader, this.syntax, this.strictVariables, this.defaultLocale, this.maxRenderedSize,
this.tagCache, this.templateCache,
this.executorService, extensionRegistry, parserOptions, evaluationOptions);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import com.mitchellbosecke.pebble.node.expression.Expression;
import com.mitchellbosecke.pebble.template.EvaluationContextImpl;
import com.mitchellbosecke.pebble.template.PebbleTemplateImpl;
import com.mitchellbosecke.pebble.utils.LimitedSizeWriter;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
Expand Down Expand Up @@ -65,7 +67,7 @@ public void render(PebbleTemplateImpl self, Writer writer,

private String render(final PebbleTemplateImpl self, final EvaluationContextImpl context)
throws IOException {
StringWriter tempWriter = new StringWriter();
Writer tempWriter = LimitedSizeWriter.from(new StringWriter(), context);
CacheNode.this.body.render(self, tempWriter, context);

return tempWriter.toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import com.mitchellbosecke.pebble.template.Macro;
import com.mitchellbosecke.pebble.template.PebbleTemplateImpl;
import com.mitchellbosecke.pebble.template.ScopeChain;
import com.mitchellbosecke.pebble.utils.LimitedSizeWriter;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
Expand Down Expand Up @@ -65,7 +67,7 @@ public String getName() {
@Override
public String call(PebbleTemplateImpl self, EvaluationContextImpl context,
Map<String, Object> macroArgs) {
Writer writer = new StringWriter();
Writer writer = LimitedSizeWriter.from(new StringWriter(), context);
ScopeChain scopeChain = context.getScopeChain();

// scope for default arguments
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import com.mitchellbosecke.pebble.node.RenderableNode;
import com.mitchellbosecke.pebble.template.EvaluationContextImpl;
import com.mitchellbosecke.pebble.template.PebbleTemplateImpl;
import com.mitchellbosecke.pebble.utils.LimitedSizeWriter;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
Expand All @@ -35,7 +37,7 @@ public RenderableNodeExpression(RenderableNode node, int lineNumber) {

@Override
public Object evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) {
Writer writer = new StringWriter();
Writer writer = LimitedSizeWriter.from(new StringWriter(), context);
try {
this.node.render(self, writer, context);
} catch (IOException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;

/**
* An evaluation context will store all stateful data that is necessary for the evaluation of a
* template. Passing the entire state around will assist with thread safety.
*
* @author Mitchell
*/
public class EvaluationContextImpl implements EvaluationContext {
public class EvaluationContextImpl implements EvaluationContext, RenderedSizeContext {

private final boolean strictVariables;

Expand All @@ -54,6 +55,11 @@ public class EvaluationContextImpl implements EvaluationContext {
*/
private final Locale locale;

/**
* The maximum size of the rendered template, in chars.
*/
private final int maxRenderedSize;

/**
* All the available filters/tests/functions for this template.
*/
Expand Down Expand Up @@ -84,6 +90,11 @@ public class EvaluationContextImpl implements EvaluationContext {
*/
private final EvaluationOptions evaluationOptions;

/**
* Total number of chars written by all writers sharing this context.
*/
private final AtomicInteger charsRendered = new AtomicInteger();

/**
* Constructor used to provide all final variables.
*
Expand All @@ -96,7 +107,7 @@ public class EvaluationContextImpl implements EvaluationContext {
* @param hierarchy The inheritance chain
* @param tagCache The cache used by the "cache" tag
*/
public EvaluationContextImpl(PebbleTemplateImpl self, boolean strictVariables, Locale locale,
public EvaluationContextImpl(PebbleTemplateImpl self, boolean strictVariables, Locale locale, int maxRenderedSize,
ExtensionRegistry extensionRegistry, PebbleCache<CacheKey, Object> tagCache,
ExecutorService executorService, List<PebbleTemplateImpl> importedTemplates,
Map<String, PebbleTemplateImpl> namedImportedTemplates, ScopeChain scopeChain,
Expand All @@ -108,6 +119,7 @@ public EvaluationContextImpl(PebbleTemplateImpl self, boolean strictVariables, L

this.strictVariables = strictVariables;
this.locale = locale;
this.maxRenderedSize = maxRenderedSize;
this.extensionRegistry = extensionRegistry;
this.tagCache = tagCache;
this.executorService = executorService;
Expand All @@ -127,7 +139,7 @@ public EvaluationContextImpl(PebbleTemplateImpl self, boolean strictVariables, L
*/
public EvaluationContextImpl shallowCopyWithoutInheritanceChain(PebbleTemplateImpl self) {
EvaluationContextImpl result = new EvaluationContextImpl(self, this.strictVariables,
this.locale, this.extensionRegistry, this.tagCache,
this.locale, this.maxRenderedSize, this.extensionRegistry, this.tagCache,
this.executorService, this.importedTemplates, this.namedImportedTemplates, this.scopeChain,
null, this.evaluationOptions);
return result;
Expand All @@ -142,7 +154,7 @@ public EvaluationContextImpl shallowCopyWithoutInheritanceChain(PebbleTemplateIm
*/
public EvaluationContextImpl threadSafeCopy(PebbleTemplateImpl self) {
EvaluationContextImpl result = new EvaluationContextImpl(self, this.strictVariables,
this.locale, this.extensionRegistry, this.tagCache,
this.locale, this.maxRenderedSize, this.extensionRegistry, this.tagCache,
this.executorService, new ArrayList<>(this.importedTemplates),
new HashMap<>(this.namedImportedTemplates), this.scopeChain.deepCopy(), this.hierarchy,
this.evaluationOptions);
Expand All @@ -169,7 +181,7 @@ public void addNamedImportedTemplates(String alias, PebbleTemplateImpl template)
/**
* Returns whether or not this template is being evaluated in "strict templates" mode
*
* @return Whether or not this template is being evaluated in "strict tempaltes" mode.
* @return Whether or not this template is being evaluated in "strict templates" mode.
*/
@Override
public boolean isStrictVariables() {
Expand All @@ -186,6 +198,15 @@ public Locale getLocale() {
return this.locale;
}

/**
* Returns the max rendered size.
* @return The max rendered size.
*/
@Override
public int getMaxRenderedSize() {
return this.maxRenderedSize;
}

/**
* Returns the extension registry used to access all of the tests/filters/functions
*
Expand Down Expand Up @@ -290,4 +311,9 @@ public void scopedShallowWithoutInheritanceChain(
scopedFunction
);
}

@Override
public int addAndGet(int delta) {
return charsRendered.addAndGet(delta);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import com.mitchellbosecke.pebble.node.RenderableNode;
import com.mitchellbosecke.pebble.node.RootNode;
import com.mitchellbosecke.pebble.utils.FutureWriter;
import com.mitchellbosecke.pebble.utils.LimitedSizeWriter;
import com.mitchellbosecke.pebble.utils.Pair;

import java.io.IOException;
Expand Down Expand Up @@ -152,6 +153,7 @@ private void evaluate(Writer writer, EvaluationContextImpl context) throws IOExc
if (context.getExecutorService() != null) {
writer = new FutureWriter(writer);
}
writer = LimitedSizeWriter.from(writer, context);
this.rootNode.render(this, writer, context);

/*
Expand Down Expand Up @@ -189,7 +191,7 @@ private EvaluationContextImpl initContext(Locale locale) {
// global vars provided from extensions
scopeChain.pushScope(this.engine.getExtensionRegistry().getGlobalVariables());

return new EvaluationContextImpl(this, this.engine.isStrictVariables(), locale,
return new EvaluationContextImpl(this, this.engine.isStrictVariables(), locale, this.engine.getMaxRenderedSize(),
this.engine.getExtensionRegistry(), this.engine.getTagCache(),
this.engine.getExecutorService(),
new ArrayList<>(), new HashMap<>(), scopeChain, null, this.engine.getEvaluationOptions());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.mitchellbosecke.pebble.template;

public interface RenderedSizeContext {
int getMaxRenderedSize();

int addAndGet(int delta);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
* ExecutorService to the main PebbleEngine. A FutureWriter is capable of handling Futures that will
* return a string.
*
* It is not thread safe but that is okay. Each thread will have it's own writer, provided by the
* It is not thread safe but that is okay. Each thread will have its own writer, provided by the
* "parallel" node; i.e. they will never share writers.
*
* @author Mitchell
Expand Down
Loading

0 comments on commit 4d27af7

Please sign in to comment.