|
| 1 | +/** |
| 2 | + * @name Cross-site scripting via HTML template escaping bypass |
| 3 | + * @description Converting user input to a special type that avoids escaping |
| 4 | + * when fed into an HTML template allows for a cross-site |
| 5 | + * scripting vulnerability. |
| 6 | + * @kind path-problem |
| 7 | + * @problem.severity error |
| 8 | + * @security-severity 6.1 |
| 9 | + * @precision high |
| 10 | + * @id go/html-template-escaping-bypass-xss |
| 11 | + * @tags security |
| 12 | + * external/cwe/cwe-079 |
| 13 | + * external/cwe/cwe-116 |
| 14 | + */ |
| 15 | + |
| 16 | +import go |
| 17 | + |
| 18 | +/** |
| 19 | + * A type that will not be escaped when passed to a `html/template` template. |
| 20 | + */ |
| 21 | +class UnescapedType extends Type { |
| 22 | + UnescapedType() { |
| 23 | + this.hasQualifiedName("html/template", |
| 24 | + ["CSS", "HTML", "HTMLAttr", "JS", "JSStr", "Srcset", "URL"]) |
| 25 | + } |
| 26 | +} |
| 27 | + |
| 28 | +/** |
| 29 | + * Holds if the sink is a data value argument of a template execution call. |
| 30 | + * |
| 31 | + * Note that this is slightly more general than |
| 32 | + * `SharedXss::HtmlTemplateSanitizer` because it uses `Function.getACall()`, |
| 33 | + * which finds calls through interfaces which the receiver implements. This |
| 34 | + * finds more results in practice. |
| 35 | + */ |
| 36 | +predicate isSinkToTemplateExec(DataFlow::Node sink) { |
| 37 | + exists(Method fn, string methodName, DataFlow::CallNode call | |
| 38 | + fn.hasQualifiedName("html/template", "Template", methodName) and |
| 39 | + call = fn.getACall() |
| 40 | + | |
| 41 | + methodName = "Execute" and sink = call.getArgument(1) |
| 42 | + or |
| 43 | + methodName = "ExecuteTemplate" and sink = call.getArgument(2) |
| 44 | + ) |
| 45 | +} |
| 46 | + |
| 47 | +/** |
| 48 | + * Data flow configuration that tracks flows from untrusted sources to template execution calls |
| 49 | + * which go through a conversion to an unescaped type. |
| 50 | + */ |
| 51 | +module UntrustedToTemplateExecWithConversionConfig implements DataFlow::StateConfigSig { |
| 52 | + private newtype TConversionState = |
| 53 | + TUnconverted() or |
| 54 | + TConverted(UnescapedType unescapedType) |
| 55 | + |
| 56 | + /** |
| 57 | + * The flow state for tracking whether a conversion to an unescaped type has |
| 58 | + * occurred. |
| 59 | + */ |
| 60 | + class FlowState extends TConversionState { |
| 61 | + predicate isBeforeConversion() { this instanceof TUnconverted } |
| 62 | + |
| 63 | + predicate isAfterConversion(UnescapedType unescapedType) { this = TConverted(unescapedType) } |
| 64 | + |
| 65 | + /** Gets a textual representation of this element. */ |
| 66 | + string toString() { |
| 67 | + this.isBeforeConversion() and result = "Unconverted" |
| 68 | + or |
| 69 | + exists(UnescapedType unescapedType | this.isAfterConversion(unescapedType) | |
| 70 | + result = "Converted to " + unescapedType.getQualifiedName() |
| 71 | + ) |
| 72 | + } |
| 73 | + } |
| 74 | + |
| 75 | + predicate isSource(DataFlow::Node source, FlowState state) { |
| 76 | + state.isBeforeConversion() and source instanceof ActiveThreatModelSource |
| 77 | + } |
| 78 | + |
| 79 | + predicate isSink(DataFlow::Node sink, FlowState state) { |
| 80 | + state.isAfterConversion(_) and isSinkToTemplateExec(sink) |
| 81 | + } |
| 82 | + |
| 83 | + predicate isBarrier(DataFlow::Node node) { |
| 84 | + node instanceof SharedXss::Sanitizer and not node instanceof SharedXss::HtmlTemplateSanitizer |
| 85 | + or |
| 86 | + node.getType() instanceof NumericType |
| 87 | + } |
| 88 | + |
| 89 | + /** |
| 90 | + * When a conversion to a passthrough type is encountered, transition the flow state. |
| 91 | + */ |
| 92 | + predicate isAdditionalFlowStep( |
| 93 | + DataFlow::Node pred, FlowState predState, DataFlow::Node succ, FlowState succState |
| 94 | + ) { |
| 95 | + exists(ConversionExpr conversion, UnescapedType unescapedType | |
| 96 | + // If not yet converted, look for a conversion to a passthrough type |
| 97 | + predState.isBeforeConversion() and |
| 98 | + succState.isAfterConversion(unescapedType) and |
| 99 | + succ.(DataFlow::TypeCastNode).getExpr() = conversion and |
| 100 | + pred.asExpr() = conversion.getOperand() and |
| 101 | + conversion.getType().getUnderlyingType*() = unescapedType |
| 102 | + ) |
| 103 | + } |
| 104 | +} |
| 105 | + |
| 106 | +module UntrustedToTemplateExecWithConversionFlow = |
| 107 | + TaintTracking::GlobalWithState<UntrustedToTemplateExecWithConversionConfig>; |
| 108 | + |
| 109 | +import UntrustedToTemplateExecWithConversionFlow::PathGraph |
| 110 | + |
| 111 | +from |
| 112 | + UntrustedToTemplateExecWithConversionFlow::PathNode untrustedSource, |
| 113 | + UntrustedToTemplateExecWithConversionFlow::PathNode templateExecCall, UnescapedType unescapedType |
| 114 | +where |
| 115 | + UntrustedToTemplateExecWithConversionFlow::flowPath(untrustedSource, templateExecCall) and |
| 116 | + templateExecCall.getState().isAfterConversion(unescapedType) |
| 117 | +select templateExecCall.getNode(), untrustedSource, templateExecCall, |
| 118 | + "Data from an $@ will not be auto-escaped because it was converted to template." + |
| 119 | + unescapedType.getName(), untrustedSource.getNode(), "untrusted source" |
0 commit comments