mirror of
https://github.com/github/codeql.git
synced 2026-01-29 22:32:58 +01:00
Add CWE-352: CSRF because of constant oauth2 state value
This commit is contained in:
26
ql/src/experimental/CWE-352/ConstantOauth2State.qhelp
Normal file
26
ql/src/experimental/CWE-352/ConstantOauth2State.qhelp
Normal 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>
|
||||
38
ql/src/experimental/CWE-352/ConstantOauth2State.ql
Normal file
38
ql/src/experimental/CWE-352/ConstantOauth2State.ql
Normal 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"
|
||||
27
ql/src/experimental/CWE-352/ConstantOauth2StateBad.go
Normal file
27
ql/src/experimental/CWE-352/ConstantOauth2StateBad.go
Normal 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)
|
||||
// ...
|
||||
}
|
||||
34
ql/src/experimental/CWE-352/ConstantOauth2StateBetter.go
Normal file
34
ql/src/experimental/CWE-352/ConstantOauth2StateBetter.go
Normal 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
|
||||
}
|
||||
19
ql/test/experimental/CWE-352/ConstantOauth2State.expected
Normal file
19
ql/test/experimental/CWE-352/ConstantOauth2State.expected
Normal 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 |
|
||||
106
ql/test/experimental/CWE-352/ConstantOauth2State.go
Normal file
106
ql/test/experimental/CWE-352/ConstantOauth2State.go
Normal 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
|
||||
}
|
||||
1
ql/test/experimental/CWE-352/ConstantOauth2State.qlref
Normal file
1
ql/test/experimental/CWE-352/ConstantOauth2State.qlref
Normal file
@@ -0,0 +1 @@
|
||||
experimental/CWE-352/ConstantOauth2State.ql
|
||||
27
ql/test/experimental/CWE-352/vendor/golang.org/x/oauth2/LICENSE
generated
vendored
Normal file
27
ql/test/experimental/CWE-352/vendor/golang.org/x/oauth2/LICENSE
generated
vendored
Normal 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.
|
||||
81
ql/test/experimental/CWE-352/vendor/golang.org/x/oauth2/stub.go
generated
vendored
Normal file
81
ql/test/experimental/CWE-352/vendor/golang.org/x/oauth2/stub.go
generated
vendored
Normal 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)
|
||||
}
|
||||
3
ql/test/experimental/CWE-352/vendor/modules.txt
vendored
Normal file
3
ql/test/experimental/CWE-352/vendor/modules.txt
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
## explicit
|
||||
golang.org/x/oauth2
|
||||
Reference in New Issue
Block a user