diff --git a/.gitattributes b/.gitattributes index fdf3185f2d5..fd2c3bc6065 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,6 @@ -# Force git not to modify line endings for go files under the ql directory +# Force git not to modify line endings for go or html files under the ql directory ql/**/*.go -text +ql/**/*.html -text # Force git not to modify line endings for dbschemes *.dbscheme -text diff --git a/Makefile b/Makefile index 29bb3321927..d4631e810cc 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ CODEQL_PLATFORM = osx64 endif endif -CODEQL_TOOLS = $(addprefix codeql-tools/,autobuild.cmd autobuild.sh index.cmd index.sh linux64 osx64 win64) +CODEQL_TOOLS = $(addprefix codeql-tools/,autobuild.cmd autobuild.sh pre-finalize.cmd pre-finalize.sh index.cmd index.sh linux64 osx64 win64) EXTRACTOR_PACK_OUT = build/codeql-extractor-go diff --git a/change-notes/2021-03-16-html-tracing.md b/change-notes/2021-03-16-html-tracing.md new file mode 100644 index 00000000000..664e5f312b6 --- /dev/null +++ b/change-notes/2021-03-16-html-tracing.md @@ -0,0 +1,2 @@ +lgtm,codescanning +* Support for extracting HTML files has been added, alongside support for Raw Revel templates. diff --git a/codeql-tools/index.cmd b/codeql-tools/index.cmd index 15d7548c1d9..21c8f64df92 100644 --- a/codeql-tools/index.cmd +++ b/codeql-tools/index.cmd @@ -2,6 +2,7 @@ SETLOCAL EnableDelayedExpansion type NUL && "%CODEQL_EXTRACTOR_GO_ROOT%/tools/%CODEQL_PLATFORM%/go-extractor.exe" -mod=vendor ./... +type NUL && "%CODEQL_EXTRACTOR_GO_ROOT%/tools/pre-finalize.cmd" exit /b %ERRORLEVEL% ENDLOCAL diff --git a/codeql-tools/index.sh b/codeql-tools/index.sh index 655fb5eeca3..877400d37f2 100755 --- a/codeql-tools/index.sh +++ b/codeql-tools/index.sh @@ -8,3 +8,4 @@ if [ "$CODEQL_PLATFORM" != "linux64" ] && [ "$CODEQL_PLATFORM" != "osx64" ] ; th fi "$CODEQL_EXTRACTOR_GO_ROOT/tools/$CODEQL_PLATFORM/go-extractor" -mod=vendor ./... +"$CODEQL_EXTRACTOR_GO_ROOT/tools/pre-finalize.sh" diff --git a/codeql-tools/pre-finalize.cmd b/codeql-tools/pre-finalize.cmd new file mode 100644 index 00000000000..4abac249933 --- /dev/null +++ b/codeql-tools/pre-finalize.cmd @@ -0,0 +1,19 @@ +@echo off +SETLOCAL EnableDelayedExpansion + +if NOT "%CODEQL_EXTRACTOR_GO_EXTRACT_HTML%"=="no" ( + type NUL && "%CODEQL_DIST%/codeql.exe" database index-files ^ + --working-dir=. ^ + --include-extension=.htm ^ + --include-extension=.html ^ + --include-extension=.xhtm ^ + --include-extension=.xhtml ^ + --include-extension=.vue ^ + --size-limit 10m ^ + --language html ^ + -- ^ + "%CODEQL_EXTRACTOR_GO_WIP_DATABASE%" ^ + || echo "HTML extraction failed; continuing" + + exit /b %ERRORLEVEL% +) diff --git a/codeql-tools/pre-finalize.sh b/codeql-tools/pre-finalize.sh new file mode 100755 index 00000000000..3a8b31c70a0 --- /dev/null +++ b/codeql-tools/pre-finalize.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +set -eu + +if [ "${CODEQL_EXTRACTOR_GO_EXTRACT_HTML:-yes}" != "no" ]; then + "$CODEQL_DIST/codeql" database index-files \ + --working-dir=. \ + --include-extension=.htm \ + --include-extension=.html \ + --include-extension=.xhtm \ + --include-extension=.xhtml \ + --include-extension=.vue \ + --size-limit 10m \ + --language html \ + -- \ + "$CODEQL_EXTRACTOR_GO_WIP_DATABASE" \ + || echo "HTML extraction failed; continuing." +fi diff --git a/extractor/dbscheme/dbscheme.go b/extractor/dbscheme/dbscheme.go index 0eadc3efd9b..fddaa3e6e1f 100644 --- a/extractor/dbscheme/dbscheme.go +++ b/extractor/dbscheme/dbscheme.go @@ -362,6 +362,15 @@ func NewUnionType(name string, parents ...*UnionType) *UnionType { return tp } +// AddChild adds the type with given `name` to the union type. +// This is useful if a type defined in a snippet should be a child of a type defined in Go. +func (parent *UnionType) AddChild(name string) bool { + tp := &PrimaryKeyType{name} + // don't add tp to types; it's expected that it's already in the db somehow. + parent.components = append(parent.components, tp) + return true +} + // NewAliasType constructs a new alias type with the given `name` that aliases `underlying` func NewAliasType(name string, underlying Type) *AliasType { tp := &AliasType{name, underlying} diff --git a/extractor/dbscheme/tables.go b/extractor/dbscheme/tables.go index 3847324b5db..233f087a2b1 100644 --- a/extractor/dbscheme/tables.go +++ b/extractor/dbscheme/tables.go @@ -44,12 +44,91 @@ snapshotDate(unique date snapshotDate : date ref); sourceLocationPrefix(varchar(900) prefix : string ref); `) +// Copied directly from the XML dbscheme +var xmlSnippet = AddDefaultSnippet(` +/* + * XML Files + */ + +xmlEncoding( + unique int id: @file ref, + string encoding: string ref +); + +xmlDTDs( + unique int id: @xmldtd, + string root: string ref, + string publicId: string ref, + string systemId: string ref, + int fileid: @file ref +); + +xmlElements( + unique int id: @xmlelement, + string name: string ref, + int parentid: @xmlparent ref, + int idx: int ref, + int fileid: @file ref +); + +xmlAttrs( + unique int id: @xmlattribute, + int elementid: @xmlelement ref, + string name: string ref, + string value: string ref, + int idx: int ref, + int fileid: @file ref +); + +xmlNs( + int id: @xmlnamespace, + string prefixName: string ref, + string URI: string ref, + int fileid: @file ref +); + +xmlHasNs( + int elementId: @xmlnamespaceable ref, + int nsId: @xmlnamespace ref, + int fileid: @file ref +); + +xmlComments( + unique int id: @xmlcomment, + string text: string ref, + int parentid: @xmlparent ref, + int fileid: @file ref +); + +xmlChars( + unique int id: @xmlcharacters, + string text: string ref, + int parentid: @xmlparent ref, + int idx: int ref, + int isCDATA: int ref, + int fileid: @file ref +); + +@xmlparent = @file | @xmlelement; +@xmlnamespaceable = @xmlelement | @xmlattribute; + +xmllocations( + int xmlElement: @xmllocatable ref, + int location: @location_default ref +); + +@xmllocatable = @xmlcharacters | @xmlelement | @xmlcomment | @xmlattribute | @xmldtd | @file | @xmlnamespace; +`) + // ContainerType is the type of files and folders var ContainerType = NewUnionType("@container") // LocatableType is the type of program entities that have locations var LocatableType = NewUnionType("@locatable") +// Adds xmllocatable as a locatable +var XmlLocatableAsLocatable = LocatableType.AddChild("@xmllocatable") + // NodeType is the type of AST nodes var NodeType = NewUnionType("@node", LocatableType) diff --git a/ql/src/Security/CWE-079/ReflectedXss.ql b/ql/src/Security/CWE-079/ReflectedXss.ql index bc11cbc7ebf..bbedd60a5fe 100644 --- a/ql/src/Security/CWE-079/ReflectedXss.ql +++ b/ql/src/Security/CWE-079/ReflectedXss.ql @@ -15,7 +15,22 @@ import go import semmle.go.security.ReflectedXss::ReflectedXss import DataFlow::PathGraph -from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink -where cfg.hasFlowPath(source, sink) -select sink.getNode(), source, sink, "Cross-site scripting vulnerability due to $@.", - source.getNode(), "user-provided value" +from + Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink, string msg, string part, + Locatable partloc +where + cfg.hasFlowPath(source, sink) and + ( + exists(string kind | kind = sink.getNode().(SharedXss::Sink).getSinkKind() | + kind = "rawtemplate" and + msg = + "Cross-site scripting vulnerability due to $@. This template argument is instantiated raw $@." and + part = "here" + ) + or + not exists(sink.getNode().(SharedXss::Sink).getSinkKind()) and + msg = "Cross-site scripting vulnerability due to $@." and + part = "" + ) and + partloc = sink.getNode().(SharedXss::Sink).getAssociatedLoc() +select sink.getNode(), source, sink, msg, source.getNode(), "user-provided value", partloc, part diff --git a/ql/src/go.dbscheme b/ql/src/go.dbscheme index 4affa49dbe2..2e92b436892 100644 --- a/ql/src/go.dbscheme +++ b/ql/src/go.dbscheme @@ -36,6 +36,80 @@ snapshotDate(unique date snapshotDate : date ref); sourceLocationPrefix(varchar(900) prefix : string ref); + +/* + * XML Files + */ + +xmlEncoding( + unique int id: @file ref, + string encoding: string ref +); + +xmlDTDs( + unique int id: @xmldtd, + string root: string ref, + string publicId: string ref, + string systemId: string ref, + int fileid: @file ref +); + +xmlElements( + unique int id: @xmlelement, + string name: string ref, + int parentid: @xmlparent ref, + int idx: int ref, + int fileid: @file ref +); + +xmlAttrs( + unique int id: @xmlattribute, + int elementid: @xmlelement ref, + string name: string ref, + string value: string ref, + int idx: int ref, + int fileid: @file ref +); + +xmlNs( + int id: @xmlnamespace, + string prefixName: string ref, + string URI: string ref, + int fileid: @file ref +); + +xmlHasNs( + int elementId: @xmlnamespaceable ref, + int nsId: @xmlnamespace ref, + int fileid: @file ref +); + +xmlComments( + unique int id: @xmlcomment, + string text: string ref, + int parentid: @xmlparent ref, + int fileid: @file ref +); + +xmlChars( + unique int id: @xmlcharacters, + string text: string ref, + int parentid: @xmlparent ref, + int idx: int ref, + int isCDATA: int ref, + int fileid: @file ref +); + +@xmlparent = @file | @xmlelement; +@xmlnamespaceable = @xmlelement | @xmlattribute; + +xmllocations( + int xmlElement: @xmllocatable ref, + int location: @location_default ref +); + +@xmllocatable = @xmlcharacters | @xmlelement | @xmlcomment | @xmlattribute | @xmldtd | @file | @xmlnamespace; + locations_default(unique int id: @location_default, int file: @file ref, int beginLine: int ref, int beginColumn: int ref, int endLine: int ref, int endColumn: int ref); @@ -133,7 +207,7 @@ has_ellipsis(int id: @callorconversionexpr ref); @container = @file | @folder; -@locatable = @node | @localscope; +@locatable = @xmllocatable | @node | @localscope; @node = @documentable | @exprparent | @modexprparent | @fieldparent | @stmtparent | @declparent | @scopenode | @comment_group | @comment; diff --git a/ql/src/go.qll b/ql/src/go.qll index 1ef249783a4..574268b2627 100644 --- a/ql/src/go.qll +++ b/ql/src/go.qll @@ -12,6 +12,7 @@ import semmle.go.Errors import semmle.go.Expr import semmle.go.Files import semmle.go.GoMod +import semmle.go.HTML import semmle.go.Locations import semmle.go.Packages import semmle.go.Scopes @@ -19,6 +20,7 @@ import semmle.go.Stmt import semmle.go.StringOps import semmle.go.Types import semmle.go.Util +import semmle.go.VariableWithFields import semmle.go.concepts.HTTP import semmle.go.controlflow.BasicBlocks import semmle.go.controlflow.ControlFlowGraph diff --git a/ql/src/semmle/go/HTML.qll b/ql/src/semmle/go/HTML.qll new file mode 100644 index 00000000000..82c8724cd4b --- /dev/null +++ b/ql/src/semmle/go/HTML.qll @@ -0,0 +1,214 @@ +/** Provides classes for working with HTML documents. */ + +import go + +module HTML { + /** + * An HTML file. + */ + class HtmlFile extends File { + HtmlFile() { this.getExtension().regexpMatch("x?html?") } + } + + /** + * An HTML element. + * + * Example: + * + * ``` + * Semmle + * ``` + */ + class Element extends Locatable, @xmlelement { + Element() { exists(HtmlFile f | xmlElements(this, _, _, _, f)) } + + override Location getLocation() { xmllocations(this, result) } + + /** + * Gets the name of this HTML element. + * + * For example, the name of `
` is `br`. + */ + string getName() { xmlElements(this, result, _, _, _) } + + /** + * Gets the parent element of this element, if any. + */ + Element getParent() { xmlElements(this, _, result, _, _) } + + /** + * Holds if this is a toplevel element, that is, if it does not have a parent element. + */ + predicate isTopLevel() { not exists(getParent()) } + + /** + * Gets the root HTML document element in which this element is contained. + */ + DocumentElement getDocument() { result = getRoot() } + + /** + * Gets the root element in which this element is contained. + */ + Element getRoot() { if isTopLevel() then result = this else result = getParent().getRoot() } + + /** + * Gets the `i`th child element (0-based) of this element. + */ + Element getChild(int i) { xmlElements(result, _, this, i, _) } + + /** + * Gets a child element of this element. + */ + Element getChild() { result = getChild(_) } + + /** + * Gets the `i`th attribute (0-based) of this element. + */ + Attribute getAttribute(int i) { xmlAttrs(result, this, _, _, i, _) } + + /** + * Gets an attribute of this element. + */ + Attribute getAnAttribute() { result = getAttribute(_) } + + /** + * Gets an attribute of this element that has the given name. + */ + Attribute getAttributeByName(string name) { + result = getAnAttribute() and + result.getName() = name + } + + /** + * Gets the text node associated with this element. + */ + TextNode getTextNode() { result.getParent() = this } + + override string toString() { result = "<" + getName() + ">..." } + } + + /** + * An attribute of an HTML element. + * + * Examples: + * + * ``` + * + * target=_blank + * >Semmle + * ``` + */ + class Attribute extends Locatable, @xmlattribute { + Attribute() { xmlAttrs(this, _, _, _, _, any(HtmlFile f)) } + + override Location getLocation() { xmllocations(this, result) } + + /** + * Gets the element to which this attribute belongs. + */ + Element getElement() { xmlAttrs(this, result, _, _, _, _) } + + /** + * Gets the root element in which the element to which this attribute + * belongs is contained. + */ + Element getRoot() { result = getElement().getRoot() } + + /** + * Gets the name of this attribute. + */ + string getName() { xmlAttrs(this, _, result, _, _, _) } + + /** + * Gets the value of this attribute. + * + * For attributes without an explicitly specified value, the + * result is the empty string. + */ + string getValue() { xmlAttrs(this, _, _, result, _, _) } + + override string toString() { result = getName() + "=" + getValue() } + } + + /** + * An HTML `` element. + * + * Example: + * + * ``` + * + * + * This is a test. + * + * + * ``` + */ + class DocumentElement extends Element { + DocumentElement() { getName() = "html" } + } + + /** + * An HTML text node. + * + * Example: + * + * ``` + *
+ * This text is represented as a text node. + *
+ * ``` + */ + class TextNode extends Locatable, @xmlcharacters { + TextNode() { exists(HtmlFile f | xmlChars(this, _, _, _, _, f)) } + + override string toString() { result = getText() } + + /** + * Gets the content of this text node. + * + * Note that entity expansion has been performed already. + */ + string getText() { xmlChars(this, result, _, _, _, _) } + + /** + * Gets the parent this text. + */ + Element getParent() { xmlChars(this, _, result, _, _, _) } + + /** + * Gets the child index number of this text node. + */ + int getIndex() { xmlChars(this, _, _, result, _, _) } + + /** + * Holds if this text node is inside a `CDATA` tag. + */ + predicate isCData() { xmlChars(this, _, _, _, 1, _) } + + override Location getLocation() { xmllocations(this, result) } + } + + /** + * An HTML comment. + * + * Example: + * + * ``` + * + * ``` + */ + class CommentNode extends Locatable, @xmlcomment { + CommentNode() { exists(HtmlFile f | xmlComments(this, _, _, f)) } + + /** Gets the element in which this comment occurs. */ + Element getParent() { xmlComments(this, _, result, _) } + + /** Gets the text of this comment, not including delimiters. */ + string getText() { result = toString().regexpCapture("(?s)", 1) } + + override string toString() { xmlComments(this, result, _, _) } + + override Location getLocation() { xmllocations(this, result) } + } +} diff --git a/ql/src/semmle/go/VariableWithFields.qll b/ql/src/semmle/go/VariableWithFields.qll new file mode 100644 index 00000000000..000543b5be1 --- /dev/null +++ b/ql/src/semmle/go/VariableWithFields.qll @@ -0,0 +1,198 @@ +/** Provides the `VariableWithFields` class, for working with variables with a chain of field or element accesses chained to it. */ + +import go + +private newtype TVariableWithFields = + TVariableRoot(Variable v) or + TVariableFieldStep(VariableWithFields base, Field f) { + exists(fieldAccessPathAux(base, f)) or exists(fieldWriteAccessPathAux(base, f)) + } or + TVariableElementStep(VariableWithFields base, string e) { + exists(elementAccessPathAux(base, e)) or exists(elementWriteAccessPathAux(base, e)) + } + +/** + * Gets a representation of the write target `wt` as a variable with fields value if there is one. + */ +private TVariableWithFields writeAccessPath(IR::WriteTarget wt) { + exists(Variable v | wt = v.getAWrite().getLhs() | result = TVariableRoot(v)) + or + exists(VariableWithFields base, Field f | wt = fieldWriteAccessPathAux(base, f) | + result = TVariableFieldStep(base, f) + ) + or + exists(VariableWithFields base, string e | wt = elementWriteAccessPathAux(base, e) | + result = TVariableElementStep(base, e) + ) +} + +/** + * Gets a representation of `insn` as a variable with fields value if there is one. + */ +private TVariableWithFields accessPath(IR::Instruction insn) { + exists(Variable v | insn = v.getARead().asInstruction() | result = TVariableRoot(v)) + or + exists(VariableWithFields base, Field f | insn = fieldAccessPathAux(base, f) | + result = TVariableFieldStep(base, f) + ) + or + exists(VariableWithFields base, string e | insn = elementAccessPathAux(base, e) | + result = TVariableElementStep(base, e) + ) +} + +/** + * Gets an IR instruction that reads a field `f` from a node that is represented + * by variable with fields value `base`. + */ +private IR::Instruction fieldAccessPathAux(TVariableWithFields base, Field f) { + exists(IR::FieldReadInstruction fr, IR::EvalInstruction frb | + fr.getBase() = frb or + fr.getBase() = IR::implicitDerefInstruction(frb.getExpr()) + | + base = accessPath(frb) and + f = fr.getField() and + result = fr + ) +} + +/** + * Gets an IR write target that represents a field `f` from a node that is represented + * by variable with fields value `base`. + */ +private IR::WriteTarget fieldWriteAccessPathAux(TVariableWithFields base, Field f) { + exists(IR::FieldTarget ft, IR::EvalInstruction ftb | + ft.getBase() = ftb or + ft.getBase() = IR::implicitDerefInstruction(ftb.getExpr()) + | + base = accessPath(ftb) and + ft.getField() = f and + result = ft + ) +} + +/** + * Gets an IR instruction that reads an element `e` from a node that is represented + * by variable with fields value `base`. + */ +private IR::Instruction elementAccessPathAux(TVariableWithFields base, string e) { + exists(IR::ElementReadInstruction er, IR::EvalInstruction erb | + er.getBase() = erb or + er.getBase() = IR::implicitDerefInstruction(erb.getExpr()) + | + base = accessPath(erb) and + e = er.getIndex().getExactValue() and + result = er + ) +} + +/** + * Gets an IR write target that represents an element `e` from a node that is represented + * by variable with fields value `base`. + */ +private IR::WriteTarget elementWriteAccessPathAux(TVariableWithFields base, string e) { + exists(IR::ElementTarget et, IR::EvalInstruction etb | + et.getBase() = etb or + et.getBase() = IR::implicitDerefInstruction(etb.getExpr()) + | + base = accessPath(etb) and + e = et.getIndex().getExactValue() and + result = et + ) +} + +/** A variable with zero or more fields or elements read from it. */ +class VariableWithFields extends TVariableWithFields { + /** + * Gets the variable corresponding to the base of this variable with fields. + * + * For example, the variable corresponding to `a` for the variable with fields + * corresponding to `a.b[c]`. + */ + Variable getBaseVariable() { this.getParent*() = TVariableRoot(result) } + + /** + * Gets the variable with fields corresponding to the parent of this variable with fields. + * + * For example, the variable with fields corresponding to `a.b` for the variable with fields + * corresponding to `a.b[c]`. + */ + VariableWithFields getParent() { + exists(VariableWithFields base | + this = TVariableFieldStep(base, _) or this = TVariableElementStep(base, _) + | + result = base + ) + } + + /** Gets a use that refers to this variable with fields. */ + DataFlow::Node getAUse() { this = accessPath(result.asInstruction()) } + + /** Gets the type of this variable with fields. */ + Type getType() { + exists(IR::Instruction acc | this = accessPath(acc) | result = acc.getResultType()) + } + + /** Gets a textual representation of this element. */ + string toString() { + exists(Variable var | this = TVariableRoot(var) | result = "(" + var + ")") + or + exists(VariableWithFields base, Field f | this = TVariableFieldStep(base, f) | + result = base + "." + f.getName() + ) + or + exists(VariableWithFields base, string e | this = TVariableElementStep(base, e) | + result = base + "[" + e + "]" + ) + } + + /** + * Gets the qualified name of the source variable or variable and fields that this represents. + * + * For example, for the variable with fields that represents the field `a.b[c]`, this would get the string + * `"a.b.c"`. + */ + string getQualifiedName() { + exists(Variable v | this = TVariableRoot(v) | result = v.getName()) + or + exists(VariableWithFields base, Field f | this = TVariableFieldStep(base, f) | + result = base.getQualifiedName() + "." + f.getName() + ) + or + exists(VariableWithFields base, string e | this = TVariableElementStep(base, e) | + result = base.getQualifiedName() + "." + e.replaceAll(".", "\\.") + ) + } + + /** + * Gets a write of this variable with fields. + */ + Write getAWrite() { this = writeAccessPath(result.getLhs()) } + + /** + * Gets the field that is the last step of this variable with fields, if any. + * + * For example, the field `c` for the variable with fields `a.b.c`. + */ + Field getField() { this = TVariableFieldStep(_, result) } + + /** + * Gets the element that this variable with fields reads, if any. + * + * For example, the string value of `c` for the variable with fields `a.b[c]`. + */ + string getElement() { this = TVariableElementStep(_, result) } + + /** + * Holds if this element is at the specified location. + * The location spans column `startcolumn` of line `startline` to + * column `endcolumn` of line `endline` in file `filepath`. + * For more information, see + * [Locations](https://help.semmle.com/QL/learn-ql/ql/locations.html). + */ + predicate hasLocationInfo( + string filepath, int startline, int startcolumn, int endline, int endcolumn + ) { + this.getBaseVariable().hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn) + } +} diff --git a/ql/src/semmle/go/concepts/HTTP.qll b/ql/src/semmle/go/concepts/HTTP.qll index 3a5b9b8ff8b..6763b6e29cb 100644 --- a/ql/src/semmle/go/concepts/HTTP.qll +++ b/ql/src/semmle/go/concepts/HTTP.qll @@ -223,6 +223,35 @@ module HTTP { DataFlow::Node getAContentTypeNode() { result = self.getAContentTypeNode() } } + /** Provides a class for modeling new HTTP template response-body APIs. */ + module TemplateResponseBody { + /** + * An expression which is written to an HTTP response body via a template execution. + * + * Extend this class to model new APIs. If you want to refine existing API models, + * extend `HTTP::ResponseBody` instead. + */ + abstract class Range extends ResponseBody::Range { + /** Gets the read of the variable inside the template where this value is read. */ + abstract HtmlTemplate::TemplateRead getRead(); + } + } + + /** + * An expression which is written to an HTTP response body via a template execution. + * + * Extend this class to refine existing API models. If you want to model new APIs, + * extend `HTTP::TemplateResponseBody::Range` instead. + */ + class TemplateResponseBody extends ResponseBody { + override TemplateResponseBody::Range self; + + TemplateResponseBody() { this = self } + + /** Gets the read of the variable inside the template where this value is read. */ + HtmlTemplate::TemplateRead getRead() { result = self.getRead() } + } + /** Provides a class for modeling new HTTP client request APIs. */ module ClientRequest { /** diff --git a/ql/src/semmle/go/frameworks/Revel.qll b/ql/src/semmle/go/frameworks/Revel.qll index 1058c54c713..23cc4edd109 100644 --- a/ql/src/semmle/go/frameworks/Revel.qll +++ b/ql/src/semmle/go/frameworks/Revel.qll @@ -104,8 +104,7 @@ module Revel { * Note these don't actually generate the response, they return a struct which is then returned by the controller * method, but it is very likely if a string is being rendered that it will end up sent to the user. * - * The `Render` and `RenderTemplate` methods are excluded for now because both execute HTML templates, and deciding - * whether a particular value is exposed unescaped or not requires parsing the template. + * The `Render` and `RenderTemplate` methods are handled by `TemplateRender` below. * * The `RenderError` method can actually return HTML content, but again only via an HTML template if one exists; * we assume it falls back to return plain text as this implies there is probably not an injection opportunity @@ -216,4 +215,114 @@ module Revel { inp = input and outp = output } } + + /** + * A read in a Revel template that uses Revel's `raw` function. + */ + class RawTemplateRead extends HtmlTemplate::TemplateRead { + RawTemplateRead() { parent.getBody().regexpMatch("(?s)raw\\s.*") } + } + + /** + * A write to a template argument field that is read raw inside of a template. + */ + private class RawTemplateArgument extends HTTP::TemplateResponseBody::Range { + RawTemplateRead read; + + RawTemplateArgument() { + exists(TemplateRender render, VariableWithFields var | + render.getRenderedFile() = read.getFile() and + // if var is a.b.c, any rhs of a write to a, a.b, or a.b.cb + this = var.getParent*().getAWrite().getRhs() + | + var.getParent*() = render.getArgumentVariable() and + ( + var = read.getReadVariable(render.getArgumentVariable()) + or + // if no write or use of that variable exists, no VariableWithFields will be generated + // so we try to find a parent VariableWithFields + // this isn't covered by the 'getParent*' above because no match would be found at all + // for var + not exists(read.getReadVariable(render.getArgumentVariable())) and + exists(string fieldName | fieldName = read.getFieldName() | + var.getQualifiedName() = + render.getArgumentVariable().getQualifiedName() + + ["." + fieldName.substring(0, fieldName.indexOf(".")), ""] + ) + ) + or + // a revel controller.Render(arg) will set controller.ViewArgs["arg"] = arg + exists(Variable arg | arg.getARead() = render.(ControllerRender).getAnArgument() | + var.getBaseVariable() = arg and + var.getQualifiedName() = read.getFieldName() + ) + ) + } + + override string getAContentType() { result = "text/html" } + + override HTTP::ResponseWriter getResponseWriter() { none() } + + override HtmlTemplate::TemplateRead getRead() { result = read } + } + + /** + * A render of a template. + */ + abstract class TemplateRender extends DataFlow::Node, TemplateInstantiation::Range { + /** Gets the name of the file that is rendered. */ + abstract File getRenderedFile(); + + /** Gets the variable passed as an argument to the template. */ + abstract VariableWithFields getArgumentVariable(); + + override DataFlow::Node getADataArgument() { result = this.getArgumentVariable().getAUse() } + } + + /** A call to `Controller.Render`. */ + private class ControllerRender extends TemplateRender, DataFlow::MethodCallNode { + ControllerRender() { this.getTarget().hasQualifiedName(packagePath(), "Controller", "Render") } + + override DataFlow::Node getTemplateArgument() { none() } + + override File getRenderedFile() { + exists(string controllerRe, string handlerRe, string pathRe | + controllerRe = "\\Q" + this.getReceiver().getType().getName() + "\\E" and + handlerRe = "\\Q" + this.getEnclosingCallable().getName() + "\\E" and + // find a file named '/views//(.