Add CWE-352: CSRF because of constant oauth2 state value

This commit is contained in:
Slavomir
2020-07-16 12:38:08 +03:00
parent 830f83f21a
commit 8cc8b8ef47
10 changed files with 362 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Oauth2 clients must implement CSRF protection for the redirection URI, which is typically accomplished by including a "state" value that binds the request to
the user's authenticated state. The Go Oauth2 library allows to specify a "state" value which is then included in the auth code URL, and then provided back by the remote authentication server in the redirect callback, from where it must be validated; failure to do so makes the client susceptible to an CSRF attack.
</p>
</overview>
<recommendation>
<p>
Always include a unique, non-guessable <code>state</code> value to the <code>AuthCodeURL</code> that is also bound to the user's authenticated state, and then validated in the redirect callback.
</p>
</recommendation>
<example>
<p>
The first example shows you the use of a constant state (bad).
</p>
<sample src="ConstantOauth2StateBad.go" />
<p>
The second example shows a better implementation idea.
</p>
<sample src="ConstantOauth2StateBetter.go" />
</example>
</qhelp>

View File

@@ -0,0 +1,38 @@
/**
* @name Use of constant `state` value in Oauth2 URL.
* @description Using a constant value for the `state` in the oauth2 URL makes the application
* susceptible to CSRF attacks.
* @kind path-problem
* @problem.severity error
* @id go/constant-oauth2-state
* @tags security
* external/cwe/cwe-352
*/
import go
import DataFlow::PathGraph
class AuthCodeURL extends Method {
AuthCodeURL() { this.hasQualifiedName("golang.org/x/oauth2", "Config", "AuthCodeURL") }
}
class FlowConf extends TaintTracking::Configuration {
FlowConf() { this = "FlowConf" }
predicate isSource(DataFlow::Node source, Literal state) {
state.isConst() and source.asExpr() = state
}
predicate isSink(DataFlow::Node sink, DataFlow::CallNode call) {
exists(AuthCodeURL m | call = m.getACall() | sink = call.getArgument(0))
}
override predicate isSource(DataFlow::Node source) { isSource(source, _) }
override predicate isSink(DataFlow::Node sink) { isSink(sink, _) }
}
from FlowConf cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "Using a constant $@ to create oauth2 URLs.", source.getNode(),
"state string"

View File

@@ -0,0 +1,27 @@
package main
import (
"fmt"
"golang.org/x/oauth2"
)
func main() {}
var stateStringVar = "state"
func badWithStringLiteralState() {
conf := &oauth2.Config{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
Scopes: []string{"SCOPE1", "SCOPE2"},
Endpoint: oauth2.Endpoint{
AuthURL: "https://provider.com/o/oauth2/auth",
TokenURL: "https://provider.com/o/oauth2/token",
},
}
url := conf.AuthCodeURL(stateStringVar, oauth2.AccessTypeOffline)
fmt.Printf("Visit the URL for the auth dialog: %v", url)
// ...
}

View File

@@ -0,0 +1,34 @@
package main
import (
"crypto/rand"
"encoding/base64"
"net/http"
"golang.org/x/oauth2"
)
func betterWithVariableStateReturned(w http.ResponseWriter) {
conf := &oauth2.Config{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
Scopes: []string{"SCOPE1", "SCOPE2"},
Endpoint: oauth2.Endpoint{
AuthURL: "https://provider.com/o/oauth2/auth",
TokenURL: "https://provider.com/o/oauth2/token",
},
}
state := generateStateOauthCookie(w)
url := conf.AuthCodeURL(state)
_ = url
// ...
}
func generateStateOauthCookie(w http.ResponseWriter) string {
b := make([]byte, 128)
rand.Read(b)
// TODO: save the state string to cookies or HTML storage.
state := base64.URLEncoding.EncodeToString(b)
return state
}

View File

@@ -0,0 +1,19 @@
edges
| ConstantOauth2State.go:15:26:15:32 | "state" : string literal | ConstantOauth2State.go:45:26:45:41 | stateStringConst |
| ConstantOauth2State.go:17:22:17:28 | "state" : string | ConstantOauth2State.go:60:26:60:39 | stateStringVar |
| ConstantOauth2State.go:75:11:75:25 | call to newFixedState : string | ConstantOauth2State.go:76:26:76:30 | state |
| ConstantOauth2State.go:81:9:81:15 | "state" : string | ConstantOauth2State.go:75:11:75:25 | call to newFixedState : string |
nodes
| ConstantOauth2State.go:15:26:15:32 | "state" : string literal | semmle.label | "state" : string literal |
| ConstantOauth2State.go:17:22:17:28 | "state" : string | semmle.label | "state" : string |
| ConstantOauth2State.go:30:26:30:32 | "state" | semmle.label | "state" |
| ConstantOauth2State.go:45:26:45:41 | stateStringConst | semmle.label | stateStringConst |
| ConstantOauth2State.go:60:26:60:39 | stateStringVar | semmle.label | stateStringVar |
| ConstantOauth2State.go:75:11:75:25 | call to newFixedState : string | semmle.label | call to newFixedState : string |
| ConstantOauth2State.go:76:26:76:30 | state | semmle.label | state |
| ConstantOauth2State.go:81:9:81:15 | "state" : string | semmle.label | "state" : string |
#select
| ConstantOauth2State.go:30:26:30:32 | "state" | ConstantOauth2State.go:30:26:30:32 | "state" | ConstantOauth2State.go:30:26:30:32 | "state" | Using a constant $@ to create oauth2 URLs. | ConstantOauth2State.go:30:26:30:32 | "state" | state string |
| ConstantOauth2State.go:45:26:45:41 | stateStringConst | ConstantOauth2State.go:15:26:15:32 | "state" : string literal | ConstantOauth2State.go:45:26:45:41 | stateStringConst | Using a constant $@ to create oauth2 URLs. | ConstantOauth2State.go:15:26:15:32 | "state" | state string |
| ConstantOauth2State.go:60:26:60:39 | stateStringVar | ConstantOauth2State.go:17:22:17:28 | "state" : string | ConstantOauth2State.go:60:26:60:39 | stateStringVar | Using a constant $@ to create oauth2 URLs. | ConstantOauth2State.go:17:22:17:28 | "state" | state string |
| ConstantOauth2State.go:76:26:76:30 | state | ConstantOauth2State.go:81:9:81:15 | "state" : string | ConstantOauth2State.go:76:26:76:30 | state | Using a constant $@ to create oauth2 URLs. | ConstantOauth2State.go:81:9:81:15 | "state" | state string |

View File

@@ -0,0 +1,106 @@
package main
//go:generate depstubber -vendor golang.org/x/oauth2 Config,Endpoint
import (
"crypto/rand"
"encoding/base64"
"net/http"
"golang.org/x/oauth2"
)
func main() {}
const stateStringConst = "state"
var stateStringVar = "state"
func badWithStringLiteralState(w http.ResponseWriter) {
conf := &oauth2.Config{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
Scopes: []string{"SCOPE1", "SCOPE2"},
Endpoint: oauth2.Endpoint{
AuthURL: "https://provider.com/o/oauth2/auth",
TokenURL: "https://provider.com/o/oauth2/token",
},
}
url := conf.AuthCodeURL("state") // BAD
_ = url
// ...
}
func badWithConstState(w http.ResponseWriter) {
conf := &oauth2.Config{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
Scopes: []string{"SCOPE1", "SCOPE2"},
Endpoint: oauth2.Endpoint{
AuthURL: "https://provider.com/o/oauth2/auth",
TokenURL: "https://provider.com/o/oauth2/token",
},
}
url := conf.AuthCodeURL(stateStringConst) // BAD
_ = url
// ...
}
func badWithFixedVarState(w http.ResponseWriter) {
conf := &oauth2.Config{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
Scopes: []string{"SCOPE1", "SCOPE2"},
Endpoint: oauth2.Endpoint{
AuthURL: "https://provider.com/o/oauth2/auth",
TokenURL: "https://provider.com/o/oauth2/token",
},
}
url := conf.AuthCodeURL(stateStringVar) // BAD
_ = url
// ...
}
func badWithFixedStateReturned(w http.ResponseWriter) {
conf := &oauth2.Config{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
Scopes: []string{"SCOPE1", "SCOPE2"},
Endpoint: oauth2.Endpoint{
AuthURL: "https://provider.com/o/oauth2/auth",
TokenURL: "https://provider.com/o/oauth2/token",
},
}
state := newFixedState()
url := conf.AuthCodeURL(state) // BAD
_ = url
// ...
}
func newFixedState() string {
return "state"
}
func betterWithVariableStateReturned(w http.ResponseWriter) {
conf := &oauth2.Config{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
Scopes: []string{"SCOPE1", "SCOPE2"},
Endpoint: oauth2.Endpoint{
AuthURL: "https://provider.com/o/oauth2/auth",
TokenURL: "https://provider.com/o/oauth2/token",
},
}
state := generateStateOauthCookie(w)
url := conf.AuthCodeURL(state) // GOOD
_ = url
// ...
}
func generateStateOauthCookie(w http.ResponseWriter) string {
b := make([]byte, 128)
rand.Read(b)
state := base64.URLEncoding.EncodeToString(b)
// TODO: save the state string to cookies or HTML storage.
return state
}

View File

@@ -0,0 +1 @@
experimental/CWE-352/ConstantOauth2State.ql

View File

@@ -0,0 +1,27 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,81 @@
// Code generated by depstubber. DO NOT EDIT.
// This is a simple stub for golang.org/x/oauth2, strictly for use in testing.
// See the LICENSE file for information about the licensing of the original library.
// Source: golang.org/x/oauth2 (exports: Config,Endpoint; functions: )
// Package oauth2 is a stub of golang.org/x/oauth2, generated by depstubber.
package oauth2
import (
context "context"
http "net/http"
time "time"
)
type AuthCodeOption interface{}
type AuthStyle int
type Config struct {
ClientID string
ClientSecret string
Endpoint Endpoint
RedirectURL string
Scopes []string
}
func (_ *Config) AuthCodeURL(_ string, _ ...AuthCodeOption) string {
return ""
}
func (_ *Config) Client(_ context.Context, _ *Token) *http.Client {
return nil
}
func (_ *Config) Exchange(_ context.Context, _ string, _ ...AuthCodeOption) (*Token, error) {
return nil, nil
}
func (_ *Config) PasswordCredentialsToken(_ context.Context, _ string, _ string) (*Token, error) {
return nil, nil
}
func (_ *Config) TokenSource(_ context.Context, _ *Token) TokenSource {
return nil
}
type Endpoint struct {
AuthURL string
TokenURL string
AuthStyle AuthStyle
}
type Token struct {
AccessToken string
TokenType string
RefreshToken string
Expiry time.Time
}
func (_ *Token) Extra(_ string) interface{} {
return nil
}
func (_ *Token) SetAuthHeader(_ *http.Request) {}
func (_ *Token) Type() string {
return ""
}
func (_ *Token) Valid() bool {
return false
}
func (_ *Token) WithExtra(_ interface{}) *Token {
return nil
}
type TokenSource interface {
Token() (*Token, error)
}

View File

@@ -0,0 +1,3 @@
# golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
## explicit
golang.org/x/oauth2