/******************************************************************************* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. 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. ******************************************************************************/ package org.apache.sling.scripting.sightly.impl.plugin; import java.util.HashMap; import java.util.HashSet; import java.util.Set; import org.apache.commons.lang.StringUtils; import org.apache.sling.scripting.sightly.compiler.RuntimeFunction; import org.apache.sling.scripting.sightly.compiler.commands.Conditional; import org.apache.sling.scripting.sightly.compiler.commands.Loop; import org.apache.sling.scripting.sightly.compiler.commands.OutText; import org.apache.sling.scripting.sightly.compiler.commands.OutputVariable; import org.apache.sling.scripting.sightly.compiler.commands.VariableBinding; import org.apache.sling.scripting.sightly.compiler.expression.Expression; import org.apache.sling.scripting.sightly.compiler.expression.ExpressionNode; import org.apache.sling.scripting.sightly.compiler.expression.MarkupContext; import org.apache.sling.scripting.sightly.compiler.expression.nodes.BinaryOperation; import org.apache.sling.scripting.sightly.compiler.expression.nodes.BinaryOperator; import org.apache.sling.scripting.sightly.compiler.expression.nodes.BooleanConstant; import org.apache.sling.scripting.sightly.compiler.expression.nodes.Identifier; import org.apache.sling.scripting.sightly.compiler.expression.nodes.MapLiteral; import org.apache.sling.scripting.sightly.compiler.expression.nodes.PropertyAccess; import org.apache.sling.scripting.sightly.compiler.expression.nodes.RuntimeCall; import org.apache.sling.scripting.sightly.compiler.expression.nodes.StringConstant; import org.apache.sling.scripting.sightly.impl.compiler.Patterns; import org.apache.sling.scripting.sightly.impl.compiler.PushStream; import org.apache.sling.scripting.sightly.impl.compiler.Syntax; import org.apache.sling.scripting.sightly.impl.compiler.frontend.CompilerContext; import org.apache.sling.scripting.sightly.impl.filter.ExpressionContext; import org.apache.sling.scripting.sightly.impl.html.MarkupUtils; /** * Implementation for the attribute plugin. */ public class AttributePlugin extends AbstractPlugin { public AttributePlugin() { name = "attribute"; priority = 150; } @Override public PluginInvoke invoke(Expression expression, PluginCallInfo callInfo, CompilerContext compilerContext) { String attributeName = decodeAttributeName(callInfo); if (attributeName != null && MarkupUtils.isSensitiveAttribute(attributeName)) { String warningMessage = String.format("Refusing to generate attribute '%s' for security reasons.", attributeName); compilerContext.getPushStream().warn(new PushStream.StreamMessage(warningMessage, expression.getRawText())); return new DefaultPluginInvoke(); //no-op invocation } return (attributeName != null) ? new SingleAttributeInvoke(attributeName, expression, compilerContext) : new MultiAttributeInvoke(expression.getRoot(), compilerContext); } private String decodeAttributeName(PluginCallInfo info) { String[] arguments = info.getArguments(); if (arguments.length == 0) { return null; } return StringUtils.join(arguments, '-'); } private final class SingleAttributeInvoke extends DefaultPluginInvoke { private final String attributeName; private final String isTrueValue; private final String escapedAttrValue; private final String shouldDisplayAttribute; private boolean writeAtEnd = true; private boolean beforeCall = true; private final String attrValue; private final ExpressionNode node; private final ExpressionNode contentNode; private SingleAttributeInvoke(String attributeName, Expression expression, CompilerContext compilerContext) { this.attributeName = attributeName; this.attrValue = compilerContext.generateVariable("attrValue_" + attributeName); this.escapedAttrValue = compilerContext.generateVariable("attrValueEscaped_" + attributeName); this.isTrueValue = compilerContext.generateVariable("isTrueValue_" + attributeName); this.shouldDisplayAttribute = compilerContext.generateVariable("shouldDisplayAttr_" + attributeName); this.node = expression.getRoot(); if (!expression.containsOption(Syntax.CONTEXT_OPTION)) { this.contentNode = escapeNodeWithHint(compilerContext, new Identifier(attrValue), MarkupContext.ATTRIBUTE, new StringConstant(attributeName)); } else { this.contentNode = new Identifier(attrValue); } } @Override public void beforeAttribute(PushStream stream, String attributeName) { if (attributeName.equals(this.attributeName)) { if (beforeCall) { emitStart(stream); } writeAtEnd = false; } } @Override public void beforeAttributeValue(PushStream stream, String attributeName, ExpressionNode attributeValue) { if (attributeName.equals(this.attributeName) && beforeCall) { emitWrite(stream); Patterns.beginStreamIgnore(stream); } } @Override public void afterAttributeValue(PushStream stream, String attributeName) { if (attributeName.equals(this.attributeName) && beforeCall) { Patterns.endStreamIgnore(stream); } } @Override public void afterAttribute(PushStream stream, String attributeName) { if (attributeName.equals(this.attributeName) && beforeCall) { emitEnd(stream); } } @Override public void afterAttributes(PushStream stream) { if (writeAtEnd) { emitStart(stream); stream.write(new OutText(" " + this.attributeName)); emitWrite(stream); emitEnd(stream); } } @Override public void onPluginCall(PushStream stream, PluginCallInfo callInfo, Expression expression) { if ("attribute".equals(callInfo.getName())) { String attributeName = decodeAttributeName(callInfo); if (this.attributeName.equals(attributeName)) { beforeCall = false; } } } private void emitStart(PushStream stream) { stream.write(new VariableBinding.Start(attrValue, node)); stream.write(new VariableBinding.Start(escapedAttrValue, contentNode)); stream.write( new VariableBinding.Start( shouldDisplayAttribute, new BinaryOperation( BinaryOperator.OR, new Identifier(escapedAttrValue), new BinaryOperation(BinaryOperator.EQ, new StringConstant("false"), new Identifier(attrValue)) ) ) ); stream.write(new Conditional.Start(shouldDisplayAttribute, true)); } private void emitWrite(PushStream stream) { stream.write(new VariableBinding.Start(isTrueValue, new BinaryOperation(BinaryOperator.EQ, new Identifier(attrValue), BooleanConstant.TRUE))); stream.write(new Conditional.Start(isTrueValue, false)); stream.write(new OutText("=\"")); stream.write(new OutputVariable(escapedAttrValue)); stream.write(new OutText("\"")); stream.write(Conditional.END); stream.write(VariableBinding.END); } private void emitEnd(PushStream stream) { stream.write(Conditional.END); stream.write(VariableBinding.END); stream.write(VariableBinding.END); stream.write(VariableBinding.END); } } private final class MultiAttributeInvoke extends DefaultPluginInvoke { private final ExpressionNode attrMap; private final String attrMapVar; private final CompilerContext compilerContext; private boolean beforeCall = true; private final Set<String> ignored = new HashSet<>(); private MultiAttributeInvoke(ExpressionNode attrMap, CompilerContext context) { this.attrMap = attrMap; this.compilerContext = context; this.attrMapVar = context.generateVariable("attrMap"); } @Override public void beforeAttributes(PushStream stream) { stream.write(new VariableBinding.Start(attrMapVar, attrMap)); } @Override public void beforeAttribute(PushStream stream, String attributeName) { ignored.add(attributeName); if (beforeCall) { String attrNameVar = compilerContext.generateVariable("attrName_" + attributeName); String attrValue = compilerContext.generateVariable("mapContains_" + attributeName); stream.write(new VariableBinding.Start(attrNameVar, new StringConstant(attributeName))); stream.write(new VariableBinding.Start(attrValue, attributeValueNode(new StringConstant(attributeName)))); writeAttribute(stream, attrNameVar, attrValue); stream.write(new Conditional.Start(attrValue, false)); } } @Override public void afterAttribute(PushStream stream, String attributeName) { if (beforeCall) { stream.write(Conditional.END); stream.write(VariableBinding.END); stream.write(VariableBinding.END); } } @Override public void onPluginCall(PushStream stream, PluginCallInfo callInfo, Expression expression) { if ("attribute".equals(callInfo.getName())) { String attrName = decodeAttributeName(callInfo); if (attrName == null) { beforeCall = false; } else { if (!beforeCall) { ignored.add(attrName); } } } } @Override public void afterAttributes(PushStream stream) { HashMap<String, ExpressionNode> ignoredLiteralMap = new HashMap<>(); for (String attr : ignored) { ignoredLiteralMap.put(attr, new BooleanConstant(true)); } MapLiteral ignoredLiteral = new MapLiteral(ignoredLiteralMap); String ignoredVar = compilerContext.generateVariable("ignoredAttributes"); stream.write(new VariableBinding.Start(ignoredVar, ignoredLiteral)); String attrNameVar = compilerContext.generateVariable("attrName"); String attrNameEscaped = compilerContext.generateVariable("attrNameEscaped"); String attrIndex = compilerContext.generateVariable("attrIndex"); stream.write(new Loop.Start(attrMapVar, attrNameVar, attrIndex)); stream.write(new VariableBinding.Start(attrNameEscaped, escapeNode(new Identifier(attrNameVar), MarkupContext.ATTRIBUTE_NAME, null))); stream.write(new Conditional.Start(attrNameEscaped, true)); String isIgnoredAttr = compilerContext.generateVariable("isIgnoredAttr"); stream.write( new VariableBinding.Start(isIgnoredAttr, new PropertyAccess(new Identifier(ignoredVar), new Identifier(attrNameVar)))); stream.write(new Conditional.Start(isIgnoredAttr, false)); String attrContent = compilerContext.generateVariable("attrContent"); stream.write(new VariableBinding.Start(attrContent, attributeValueNode(new Identifier(attrNameVar)))); writeAttribute(stream, attrNameEscaped, attrContent); stream.write(VariableBinding.END); //end of attrContent stream.write(Conditional.END); stream.write(VariableBinding.END); stream.write(Conditional.END); stream.write(VariableBinding.END); stream.write(Loop.END); stream.write(VariableBinding.END); stream.write(VariableBinding.END); } private void writeAttribute(PushStream stream, String attrNameVar, String attrContentVar) { String escapedContent = compilerContext.generateVariable("attrContentEscaped"); String shouldDisplayAttribute = compilerContext.generateVariable("shouldDisplayAttr"); stream.write(new VariableBinding.Start(escapedContent, escapedExpression(new Identifier(attrContentVar), new Identifier(attrNameVar)))); stream.write( new VariableBinding.Start( shouldDisplayAttribute, new BinaryOperation( BinaryOperator.OR, new Identifier(escapedContent), new BinaryOperation(BinaryOperator.EQ, new StringConstant("false"), new Identifier(attrContentVar)) ) ) ); stream.write(new Conditional.Start(shouldDisplayAttribute, true)); stream.write(new OutText(" ")); //write("attrName"); writeAttributeName(stream, attrNameVar); writeAttributeValue(stream, escapedContent, attrContentVar); stream.write(Conditional.END); stream.write(VariableBinding.END); stream.write(VariableBinding.END); } private void writeAttributeName(PushStream stream, String attrNameVar) { stream.write(new OutputVariable(attrNameVar)); } private void writeAttributeValue(PushStream stream, String escapedContent, String attrContentVar) { String isTrueVar = compilerContext.generateVariable("isTrueAttr"); // holds the comparison (attrValue == true) stream.write(new VariableBinding.Start(isTrueVar, //isTrueAttr = (attrContent == true) new BinaryOperation(BinaryOperator.EQ, new Identifier(attrContentVar), BooleanConstant.TRUE))); stream.write(new Conditional.Start(isTrueVar, false)); //if (!isTrueAttr) stream.write(new OutText("=\"")); stream.write(new OutputVariable(escapedContent)); //write(escapedContent) stream.write(new OutText("\"")); stream.write(Conditional.END); //end if isTrueAttr stream.write(VariableBinding.END); //end scope for isTrueAttr } private ExpressionNode attributeValueNode(ExpressionNode attributeNameNode) { return new PropertyAccess(new Identifier(attrMapVar), attributeNameNode); } private ExpressionNode escapedExpression(ExpressionNode original, ExpressionNode hint) { return escapeNode(original, MarkupContext.ATTRIBUTE, hint); } private ExpressionNode escapeNode(ExpressionNode node, MarkupContext markupContext, ExpressionNode hint) { return escapeNodeWithHint(compilerContext, node, markupContext, hint); } } private static ExpressionNode escapeNodeWithHint(CompilerContext compilerContext, ExpressionNode node, MarkupContext markupContext, ExpressionNode hint) { if (hint != null) { //todo: this is not the indicated way to escape via XSS. Correct after modifying the compiler context API return new RuntimeCall(RuntimeFunction.XSS, node, new StringConstant(markupContext.getName()), hint); } return compilerContext.adjustToContext(new Expression(node), markupContext, ExpressionContext.ATTRIBUTE).getRoot(); } }