FreshMarker Ausgabeformate und Autoescaping

Jede Form ist ein Kerker.

Friedrich Hebbel

Bei der Verwendung einer Template-Engine ist die größte Herausforderung das Einfügen der variablen Inhalte in das Dokument. Hierbei ist neben der technischen Realisierung auch die Sicherheit zu beachten. Allzu leicht könnten sonst bösartige Inhalte in das generierte Dokument gelangen. Je nach Verwendung als E-Mail, Webseite oder Konfiguration wären so diverse Angriffsszenarien denkbar.

Das Problem der Absicherung gegen jede Art von Injektion wird in FreshMarker durch die Verwendung spezieller Ausgabeformate umgesetzt. Je nach Ausgabeformat werden dann spezielle Zeichen in den Interpolations maskiert.

Es existieren mehrere Möglichkeiten um das Ausgabeformat für ein Template zu setzen. Über die Konfiguration kann das Standardformat für alle Templates vorbelegt werden.

StringTemplateLoader templateLoader = new StringTemplateLoader();
templateLoader.putTemplate("test", "<P>${content}</P>");

Configuration configuration = new Configuration();
configuration.registerTemplateLoader(templateLoader);
configuration.setOutputFormat("HTML");

Template template = configuration.getTemplate("test");
System.out.println(template.process(Map.of("content", "<br/>")));

In diesem Beispiel wird das Ausgabeformat HTML als Standardformat in der Konfiguration gesetzt. Damit werden die Zeichen <, >, " und ' durch &lt;, &gt;, &quot; und &#39; ersetzt. Die Ausgabe des Beispiels lautet <P>&lt;br/&gt;</P>. Neben dem Ausgabeformat HTML werden auch XHTML, XML unterstützt. es können auch plainText, undefined, JavaScript, JSON und CSS angegeben werden. Diese führen jedoch keinerlei Maskierung durch.

Für einen Block innerhalb eines Templates kann das Ausgabeformat über die Direktive outputformat geändert werden.

${content}
<#outputformat 'HTML'>
${content}
</#outputformat>
${content}

In diesem Beispiel wird nur der Inhalt der Direktive mit dem Ausgabeformat HTML maskiert und die Interpolations davor und danach werden mit dem Standardformat eingefügt. Die Ausgabe des Beispiels für das Standardformat plainText lautet <br/>&tl;br/&gt;<br/>.

<#outputformat 'HTML'>
${content?noEsc}
</#outputformat>

Soll für eine Interpolation keine Maskierung erfolgen, dann kann dies über das Built-In noEsc gesteuert werden. Obwohl das Ausgabeformat HTML gewählt wurde, wird über das Built-In die Maskierung deaktiviert und die Ausgabe des Beispiels ist <br/>.

Das Herzstück der Implementierung besteht aus der Datenmodell Ergänzung TemplateMarkup. Diese Klasse speichert Text und das dazu gewünschte Ausgabeformat. Bei der Generierung einer InterpolationFragment Instanz wird der Interpolation Ausdruck von einer TemplateMarkup Instanz umfasst,

@Override
public BlockFragment visit(Interpolation ftl, BlockFragment input) {
  logger.debug("interpolation: {}", ftl);
  TemplateObject interpolation = ftl.getChild(1).accept(interpolationBuilder, null);
  input.addFragment(new InterpolationFragment(new TemplateMarkup(interpolation), ftl));
  return input;
}

Der Konstruktor mit nur einem Parameter verwendet ein spezielles Ausgabeformat DelegatingOutputFormat.INSTANCE. Dieses Ausgabeformat delegiert die Maskierung an das aktuell im Environment eingestellte Ausgabeformat. Ist das Standardformat beispielsweise XML, dann werden solche TemplateMarkup Instanzen mit dem HtmlOutputFormat.XML maskiert.

Innerhalb der Klasse TemplateMarkup wird das eingebettete TemplateObject ausgewertet und wenn es sich nicht wiederum um ein TemplateMarkup handelt mit dem entsprechenden Formatter in Text umgewandelt und dann von dem aktuellen Ausgabeformat maskiert.

@Override
public TemplateObject evaluateToObject(ProcessContext context) {
  TemplateObject templateObject = getTemplateObject(context);
  if (templateObject.isMarkup()) {
    return templateObject.evaluate(context, TemplateString.class);
  }
  Environment environment = context.getEnvironment();
  String result = context.getFormatter(templateObject.getClass()).format(templateObject, environment.getLocale());
  return outputFormat.escape(environment, result);
}

Handelt es sich bei dem eingebetteten TemplateObject um eine TemplateMarkup Instanz, dann wird diese ausgewertet und ihr Inhalt direkt zurückgegeben. Auf diese Weise werden die outputformat Direktive und das noEsc Built-In realisiert.

Die outputformat Direktive erzeugt ein spezielle OutputFormatFragment für das Template, mit dem für das eingebettete BlockFragment das AusgabeFormat im Environment gewechselt wird.

public class OutputFormatFragment implements Fragment {

  private final BlockFragment content;
  private final String format;

  public OutputFormatFragment(BlockFragment content, String format) {
    this.content = content;
    this.format = format;
  }

  @Override
  public void process(ProcessContext context) {
    Environment environment = context.getEnvironment();
    context.setEnvironment(new SettingEnvironment(environment, null, context.getOutputFormat(format)));
    content.process(context);
    context.setEnvironment(environment);
  }
}

Das Built-In noEsc erzeugt für eine TemplateString Instanz eine TemplateMarkup Instanz, die das Ausgabeformat NoEscapeFormat.INSTANCE verwendet.

 @BuiltInMethod
public static TemplateMarkup noEsc(TemplateString value) {
  return new TemplateMarkup(value, NoEscapeFormat.INSTANCE);
}

Wie der Name erahnen lässt, nimmt dieses Ausgabeformat keinerlei Maskierungen des Textes vor.

Damit ist der Mechanismus für das Maskieren von Zeichen in der FreshMarker Ausgabe auch schon beschrieben. Momentan existiert noch keine Möglichkeit, zusätzliche Ausgabeformate zu definieren, aber das ist der Inhalt eines zukünftigen Beitrags.

Leave a Comment