Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion go/ql/src/Security/CWE-020/IncompleteHostnameRegexp.ql
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,25 @@

import go

/** Escapes dots within quoted sections for the hostname pattern check. */
bindingset[pattern]
private string escapeQuotedDots(string pattern) {
if not pattern.matches("%\\Q%")
then result = pattern
else
result =
concat(string part, string escapedPart, int i |
// Consume quoted sections before other escapes. Outside quoting, paired
// backslashes cannot open a section; inside, the first \E closes it.
part = pattern.regexpFind("\\\\Q[\\s\\S]*?(?:\\\\E|\\z)|\\\\[^Q]|[^\\\\]+|\\\\\\z", i, _) and
if part.prefix(2) = "\\Q"
then escapedPart = part.replaceAll(".", "\\.")
else escapedPart = part
|
escapedPart, "" order by i
)
}

/**
* Holds if `pattern` is a regular expression pattern for URLs with a host matched by `hostPart`,
* and `pattern` contains a subtle mistake that allows it to match unexpected hosts.
Expand Down Expand Up @@ -114,7 +133,21 @@ import Flow::PathGraph
from Flow::PathNode source, Flow::PathNode sink, string hostPart
where
Flow::flowPath(source, sink) and
IncompleteHostNameRegexpConfig::isSourceString(source.getNode(), hostPart)
// A preceding concatenation can escape a quote opener in a source fragment.
// Interpret quoting only when the complete pattern is known.
if
exists(string pattern |
pattern = sink.getNode().(RegexpPattern).getPattern() and
pattern.matches("%\\Q%")
)
then
exists(string pattern |
pattern = sink.getNode().(RegexpPattern).getPattern() and
pattern = source.getNode().asExpr().getStringValue()
|
isIncompleteHostNameRegexpPattern(escapeQuotedDots(pattern), hostPart)
)
else IncompleteHostNameRegexpConfig::isSourceString(source.getNode(), hostPart)
select source, source, sink,
"This regular expression has an unescaped dot before '" + hostPart + "', " +
"so it might match more hosts than expected when $@.", sink, "the regular expression is used"
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* The `go/incomplete-hostname-regexp` query now recognizes `\Q...\E` quoted text in constant regular expression patterns, avoiding false positives for literal hostname dots.
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,37 @@
| main.go:49:21:49:45 | `https://www.example.com` | main.go:49:21:49:45 | `https://www.example.com` | main.go:65:15:65:23 | localVar3 | This regular expression has an unescaped dot before 'example.com', so it might match more hosts than expected when $@. | main.go:65:15:65:23 | localVar3 | the regular expression is used |
| main.go:56:15:56:34 | ...+... | main.go:56:15:56:34 | ...+... | main.go:56:15:56:34 | ...+... | This regular expression has an unescaped dot before 'example.com', so it might match more hosts than expected when $@. | main.go:56:15:56:34 | ...+... | the regular expression is used |
| main.go:58:15:58:42 | ...+... | main.go:58:15:58:42 | ...+... | main.go:58:15:58:42 | ...+... | This regular expression has an unescaped dot before 'example.com', so it might match more hosts than expected when $@. | main.go:58:15:58:42 | ...+... | the regular expression is used |
| quoted-hostnames.go:10:21:10:43 | `^https://github.com/$` | quoted-hostnames.go:10:21:10:43 | `^https://github.com/$` | quoted-hostnames.go:10:21:10:43 | `^https://github.com/$` | This regular expression has an unescaped dot before 'com', so it might match more hosts than expected when $@. | quoted-hostnames.go:10:21:10:43 | `^https://github.com/$` | the regular expression is used |
| quoted-hostnames.go:16:21:16:47 | `^https://\\Qgithub\\E.com/$` | quoted-hostnames.go:16:21:16:47 | `^https://\\Qgithub\\E.com/$` | quoted-hostnames.go:16:21:16:47 | `^https://\\Qgithub\\E.com/$` | This regular expression has an unescaped dot before 'com', so it might match more hosts than expected when $@. | quoted-hostnames.go:16:21:16:47 | `^https://\\Qgithub\\E.com/$` | the regular expression is used |
| quoted-hostnames.go:19:21:19:72 | `^(?:\\Qhttps://github.com/\\E\|https://example.org/)$` | quoted-hostnames.go:19:21:19:72 | `^(?:\\Qhttps://github.com/\\E\|https://example.org/)$` | quoted-hostnames.go:19:21:19:72 | `^(?:\\Qhttps://github.com/\\E\|https://example.org/)$` | This regular expression has an unescaped dot before 'org', so it might match more hosts than expected when $@. | quoted-hostnames.go:19:21:19:72 | `^(?:\\Qhttps://github.com/\\E\|https://example.org/)$` | the regular expression is used |
| quoted-hostnames.go:28:21:28:46 | `^\\\\Qhttps://github.com/$` | quoted-hostnames.go:28:21:28:46 | `^\\\\Qhttps://github.com/$` | quoted-hostnames.go:28:21:28:46 | `^\\\\Qhttps://github.com/$` | This regular expression has an unescaped dot before 'com', so it might match more hosts than expected when $@. | quoted-hostnames.go:28:21:28:46 | `^\\\\Qhttps://github.com/$` | the regular expression is used |
| quoted-hostnames.go:34:21:34:74 | `^(?:\\Qhttps://github\\Q.com/\\E\|https://example.org/)$` | quoted-hostnames.go:34:21:34:74 | `^(?:\\Qhttps://github\\Q.com/\\E\|https://example.org/)$` | quoted-hostnames.go:34:21:34:74 | `^(?:\\Qhttps://github\\Q.com/\\E\|https://example.org/)$` | This regular expression has an unescaped dot before 'org', so it might match more hosts than expected when $@. | quoted-hostnames.go:34:21:34:74 | `^(?:\\Qhttps://github\\Q.com/\\E\|https://example.org/)$` | the regular expression is used |
| quoted-hostnames.go:37:21:37:48 | `^\\Qhttps://github\\\\E.com/$` | quoted-hostnames.go:37:21:37:48 | `^\\Qhttps://github\\\\E.com/$` | quoted-hostnames.go:37:21:37:48 | `^\\Qhttps://github\\\\E.com/$` | This regular expression has an unescaped dot before 'com', so it might match more hosts than expected when $@. | quoted-hostnames.go:37:21:37:48 | `^\\Qhttps://github\\\\E.com/$` | the regular expression is used |
| quoted-hostnames.go:46:21:46:49 | "^https://\\\\Qgithub\\\\E.com/$" | quoted-hostnames.go:46:21:46:49 | "^https://\\\\Qgithub\\\\E.com/$" | quoted-hostnames.go:46:21:46:49 | "^https://\\\\Qgithub\\\\E.com/$" | This regular expression has an unescaped dot before 'com', so it might match more hosts than expected when $@. | quoted-hostnames.go:46:21:46:49 | "^https://\\\\Qgithub\\\\E.com/$" | the regular expression is used |
| quoted-hostnames.go:53:28:53:54 | `^https://\\Qgithub\\E.com/$` | quoted-hostnames.go:53:28:53:54 | `^https://\\Qgithub\\E.com/$` | quoted-hostnames.go:61:21:61:38 | wildcardAfterQuote | This regular expression has an unescaped dot before 'com', so it might match more hosts than expected when $@. | quoted-hostnames.go:61:21:61:38 | wildcardAfterQuote | the regular expression is used |
| quoted-hostnames.go:53:28:53:54 | `^https://\\Qgithub\\E.com/$` | quoted-hostnames.go:53:28:53:54 | `^https://\\Qgithub\\E.com/$` | quoted-hostnames.go:62:17:62:34 | wildcardAfterQuote | This regular expression has an unescaped dot before 'com', so it might match more hosts than expected when $@. | quoted-hostnames.go:62:17:62:34 | wildcardAfterQuote | the regular expression is used |
| quoted-hostnames.go:53:28:53:54 | `^https://\\Qgithub\\E.com/$` | quoted-hostnames.go:53:28:53:54 | `^https://\\Qgithub\\E.com/$` | quoted-hostnames.go:63:21:63:38 | wildcardAfterQuote | This regular expression has an unescaped dot before 'com', so it might match more hosts than expected when $@. | quoted-hostnames.go:63:21:63:38 | wildcardAfterQuote | the regular expression is used |
| quoted-hostnames.go:69:12:69:37 | `\\Q)?https://github.com/$` | quoted-hostnames.go:69:12:69:37 | `\\Q)?https://github.com/$` | quoted-hostnames.go:70:21:70:35 | ...+... | This regular expression has an unescaped dot before 'com', so it might match more hosts than expected when $@. | quoted-hostnames.go:70:21:70:35 | ...+... | the regular expression is used |
| quoted-hostnames.go:73:30:73:55 | `\\Q)?https://github.com/$` | quoted-hostnames.go:73:30:73:55 | `\\Q)?https://github.com/$` | quoted-hostnames.go:73:21:73:55 | ...+... | This regular expression has an unescaped dot before 'com', so it might match more hosts than expected when $@. | quoted-hostnames.go:73:21:73:55 | ...+... | the regular expression is used |
| quoted-hostnames.go:76:21:76:56 | ...+... | quoted-hostnames.go:76:21:76:56 | ...+... | quoted-hostnames.go:76:21:76:56 | ...+... | This regular expression has an unescaped dot before 'com', so it might match more hosts than expected when $@. | quoted-hostnames.go:76:21:76:56 | ...+... | the regular expression is used |
edges
| IncompleteHostnameRegexp.go:11:8:11:36 | "^((www\|beta).)?example.com/" | IncompleteHostnameRegexp.go:12:38:12:39 | re | provenance | Sink:MaD:2 |
| IncompleteHostnameRegexp.go:11:8:11:36 | "^((www\|beta).)?example.com/" | IncompleteHostnameRegexp.go:12:38:12:39 | re | provenance | Sink:MaD:3 |
| main.go:49:21:49:45 | `https://www.example.com` | main.go:62:15:62:25 | sourceConst | provenance | |
| main.go:62:15:62:25 | sourceConst | main.go:65:15:65:23 | localVar3 | provenance | Sink:MaD:1 |
| main.go:62:15:62:25 | sourceConst | main.go:65:15:65:23 | localVar3 | provenance | Sink:MaD:2 |
| quoted-hostnames.go:52:24:52:50 | `^\\Qhttps://github.com/\\E$` | quoted-hostnames.go:57:21:57:34 | quotedHostname | provenance | Sink:MaD:4 |
| quoted-hostnames.go:52:24:52:50 | `^\\Qhttps://github.com/\\E$` | quoted-hostnames.go:58:17:58:30 | quotedHostname | provenance | Sink:MaD:1 |
| quoted-hostnames.go:52:24:52:50 | `^\\Qhttps://github.com/\\E$` | quoted-hostnames.go:59:21:59:34 | quotedHostname | provenance | Sink:MaD:3 |
| quoted-hostnames.go:53:28:53:54 | `^https://\\Qgithub\\E.com/$` | quoted-hostnames.go:61:21:61:38 | wildcardAfterQuote | provenance | Sink:MaD:4 |
| quoted-hostnames.go:53:28:53:54 | `^https://\\Qgithub\\E.com/$` | quoted-hostnames.go:62:17:62:34 | wildcardAfterQuote | provenance | Sink:MaD:1 |
| quoted-hostnames.go:53:28:53:54 | `^https://\\Qgithub\\E.com/$` | quoted-hostnames.go:63:21:63:38 | wildcardAfterQuote | provenance | Sink:MaD:3 |
| quoted-hostnames.go:69:12:69:37 | `\\Q)?https://github.com/$` | quoted-hostnames.go:70:30:70:35 | suffix | provenance | |
| quoted-hostnames.go:70:30:70:35 | suffix | quoted-hostnames.go:70:21:70:35 | ...+... | provenance | Config Sink:MaD:4 |
| quoted-hostnames.go:73:30:73:55 | `\\Q)?https://github.com/$` | quoted-hostnames.go:73:21:73:55 | ...+... | provenance | Config Sink:MaD:4 |
models
| 1 | Sink: regexp; ; false; Match; ; ; Argument[0]; regex-use[1]; manual |
| 2 | Sink: regexp; ; false; MatchString; ; ; Argument[0]; regex-use[1]; manual |
| 1 | Sink: regexp; ; false; Compile; ; ; Argument[0]; regex-use[c]; manual |
| 2 | Sink: regexp; ; false; Match; ; ; Argument[0]; regex-use[1]; manual |
| 3 | Sink: regexp; ; false; MatchString; ; ; Argument[0]; regex-use[1]; manual |
| 4 | Sink: regexp; ; false; MustCompile; ; ; Argument[0]; regex-use[c]; manual |
nodes
| IncompleteHostnameRegexp.go:11:8:11:36 | "^((www\|beta).)?example.com/" | semmle.label | "^((www\|beta).)?example.com/" |
| IncompleteHostnameRegexp.go:12:38:12:39 | re | semmle.label | re |
Expand All @@ -22,4 +46,32 @@ nodes
| main.go:58:15:58:42 | ...+... | semmle.label | ...+... |
| main.go:62:15:62:25 | sourceConst | semmle.label | sourceConst |
| main.go:65:15:65:23 | localVar3 | semmle.label | localVar3 |
| quoted-hostnames.go:7:21:7:87 | `^// TODO (\\Qhttps://github.com/FerretDB/\\E([-\\w]+)/issues/(\\d+))$` | semmle.label | `^// TODO (\\Qhttps://github.com/FerretDB/\\E([-\\w]+)/issues/(\\d+))$` |
| quoted-hostnames.go:10:21:10:43 | `^https://github.com/$` | semmle.label | `^https://github.com/$` |
| quoted-hostnames.go:16:21:16:47 | `^https://\\Qgithub\\E.com/$` | semmle.label | `^https://\\Qgithub\\E.com/$` |
| quoted-hostnames.go:19:21:19:72 | `^(?:\\Qhttps://github.com/\\E\|https://example.org/)$` | semmle.label | `^(?:\\Qhttps://github.com/\\E\|https://example.org/)$` |
| quoted-hostnames.go:22:21:22:76 | `^(?:\\Qhttps://github.com/\\E\|\\Qhttps://example.org/\\E)$` | semmle.label | `^(?:\\Qhttps://github.com/\\E\|\\Qhttps://example.org/\\E)$` |
| quoted-hostnames.go:25:21:25:44 | `^\\Qhttps://github.com/` | semmle.label | `^\\Qhttps://github.com/` |
| quoted-hostnames.go:28:21:28:46 | `^\\\\Qhttps://github.com/$` | semmle.label | `^\\\\Qhttps://github.com/$` |
| quoted-hostnames.go:31:21:31:49 | `^\\\\\\Qhttps://github.com/\\E$` | semmle.label | `^\\\\\\Qhttps://github.com/\\E$` |
| quoted-hostnames.go:34:21:34:74 | `^(?:\\Qhttps://github\\Q.com/\\E\|https://example.org/)$` | semmle.label | `^(?:\\Qhttps://github\\Q.com/\\E\|https://example.org/)$` |
| quoted-hostnames.go:37:21:37:48 | `^\\Qhttps://github\\\\E.com/$` | semmle.label | `^\\Qhttps://github\\\\E.com/$` |
| quoted-hostnames.go:40:21:40:66 | `^\\Qhttps://github.com/\\ehttps://example.org/` | semmle.label | `^\\Qhttps://github.com/\\ehttps://example.org/` |
| quoted-hostnames.go:43:21:43:49 | "^\\\\Qhttps://github.com/\\\\E$" | semmle.label | "^\\\\Qhttps://github.com/\\\\E$" |
| quoted-hostnames.go:46:21:46:49 | "^https://\\\\Qgithub\\\\E.com/$" | semmle.label | "^https://\\\\Qgithub\\\\E.com/$" |
| quoted-hostnames.go:49:21:49:63 | `^\\Qhttps://github.com/\\E\\Qexample.org/\\E$` | semmle.label | `^\\Qhttps://github.com/\\E\\Qexample.org/\\E$` |
| quoted-hostnames.go:52:24:52:50 | `^\\Qhttps://github.com/\\E$` | semmle.label | `^\\Qhttps://github.com/\\E$` |
| quoted-hostnames.go:53:28:53:54 | `^https://\\Qgithub\\E.com/$` | semmle.label | `^https://\\Qgithub\\E.com/$` |
| quoted-hostnames.go:57:21:57:34 | quotedHostname | semmle.label | quotedHostname |
| quoted-hostnames.go:58:17:58:30 | quotedHostname | semmle.label | quotedHostname |
| quoted-hostnames.go:59:21:59:34 | quotedHostname | semmle.label | quotedHostname |
| quoted-hostnames.go:61:21:61:38 | wildcardAfterQuote | semmle.label | wildcardAfterQuote |
| quoted-hostnames.go:62:17:62:34 | wildcardAfterQuote | semmle.label | wildcardAfterQuote |
| quoted-hostnames.go:63:21:63:38 | wildcardAfterQuote | semmle.label | wildcardAfterQuote |
| quoted-hostnames.go:69:12:69:37 | `\\Q)?https://github.com/$` | semmle.label | `\\Q)?https://github.com/$` |
| quoted-hostnames.go:70:21:70:35 | ...+... | semmle.label | ...+... |
| quoted-hostnames.go:70:30:70:35 | suffix | semmle.label | suffix |
| quoted-hostnames.go:73:21:73:55 | ...+... | semmle.label | ...+... |
| quoted-hostnames.go:73:30:73:55 | `\\Q)?https://github.com/$` | semmle.label | `\\Q)?https://github.com/$` |
| quoted-hostnames.go:76:21:76:56 | ...+... | semmle.label | ...+... |
subpaths
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package main

import "regexp"

func quotedHostnames() {
// The hostname in the original issue is literal text.
regexp.MustCompile(`^// TODO (\Qhttps://github.com/FerretDB/\E([-\w]+)/issues/(\d+))$`)

// An ordinary wildcard dot remains unsafe.
regexp.MustCompile(`^https://github.com/$`) // $ Alert

// An explicitly escaped dot is literal.
regexp.MustCompile(`^https://github\.com/$`)

// A dot after the quoted region remains a wildcard.
regexp.MustCompile(`^https://\Qgithub\E.com/$`) // $ Alert

// A quoted hostname must not hide a later unsafe alternative.
regexp.MustCompile(`^(?:\Qhttps://github.com/\E|https://example.org/)$`) // $ Alert

// Both alternatives are literal, in separate quoted regions.
regexp.MustCompile(`^(?:\Qhttps://github.com/\E|\Qhttps://example.org/\E)$`)

// An unterminated quoted region extends to the end of the pattern.
regexp.MustCompile(`^\Qhttps://github.com/`)

// Two backslashes escape the opener, leaving the dot as a wildcard.
regexp.MustCompile(`^\\Qhttps://github.com/$`) // $ Alert

// Three backslashes leave a real opener after a literal backslash.
regexp.MustCompile(`^\\\Qhttps://github.com/\E$`)

// Another opener is literal text; the first closing marker still ends quoting.
regexp.MustCompile(`^(?:\Qhttps://github\Q.com/\E|https://example.org/)$`) // $ Alert

// The first literal closing marker ends quoting even after another backslash.
regexp.MustCompile(`^\Qhttps://github\\E.com/$`) // $ Alert

// A lowercase closing marker is literal and leaves the region open.
regexp.MustCompile(`^\Qhttps://github.com/\ehttps://example.org/`)

// Interpreted Go strings decode to the same quoted pattern.
regexp.MustCompile("^\\Qhttps://github.com/\\E$")

// Decoding an interpreted string does not quote the following wildcard.
regexp.MustCompile("^https://\\Qgithub\\E.com/$") // $ Alert

// Adjacent quoted sections remain literal.
regexp.MustCompile(`^\Qhttps://github.com/\E\Qexample.org/\E$`)
}

const quotedHostname = `^\Qhttps://github.com/\E$`
const wildcardAfterQuote = `^https://\Qgithub\E.com/$` // $ Alert

func constantQuotePatterns() {
// Constant aliases supply complete patterns to compilation and matching APIs.
regexp.MustCompile(quotedHostname)
regexp.Compile(quotedHostname)
regexp.MatchString(quotedHostname, "https://github.com/")

regexp.MustCompile(wildcardAfterQuote) // $ Sink
regexp.Compile(wildcardAfterQuote) // $ Sink
regexp.MatchString(wildcardAfterQuote, "https://github.com/") // $ Sink
}

func concatenatedQuoteOpeners() {
// The prefix escapes the suffix's opener, leaving its hostname dot unsafe.
prefix := `^(?:\`
suffix := `\Q)?https://github.com/$` // $ Alert
regexp.MustCompile(prefix + suffix) // $ Sink

// The same context matters when the suffix is inline.
regexp.MustCompile(prefix + `\Q)?https://github.com/$`) // $ Alert

// A constant concatenation supplies the complete pattern.
regexp.MustCompile(`^(?:\` + `\Q)?https://github.com/$`) // $ Alert
}