diff --git a/.idea/govwa.iml b/.idea/govwa.iml new file mode 100644 index 00000000..7ee078df --- /dev/null +++ b/.idea/govwa.iml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 00000000..add96c5e --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + { + "associatedIndex": 5 +} + + + + { + "keyToString": { + "RunOnceActivity.OpenProjectViewOnStart": "true", + "RunOnceActivity.ShowReadmeOnStart": "true", + "RunOnceActivity.go.formatter.settings.were.checked": "true", + "RunOnceActivity.go.migrated.go.modules.settings": "true", + "RunOnceActivity.go.modules.automatic.dependencies.download": "true", + "RunOnceActivity.go.modules.go.list.on.any.changes.was.set": "true", + "git-widget-placeholder": "owasp-rules", + "go.import.settings.migrated": "true", + "go.sdk.automatically.set": "true", + "last_opened_file_path": "/Users/nergin/Documents/workspaces/govwa", + "node.js.detected.package.eslint": "true", + "node.js.selected.package.eslint": "(autodetect)", + "nodejs_package_manager_path": "npm" + } +} + + + + + + + + + + + + + + + + + true + + \ No newline at end of file diff --git a/README.md b/README.md index 3839166c..6fea5200 100644 --- a/README.md +++ b/README.md @@ -101,3 +101,4 @@ Explore the vulnerability. * Build Simple Android APP Powered by [NemoSecurity](https://nemosecurity.com) +Powered by [NemoSecurity](https://nemosecurity.com) diff --git a/go/aws-lambda/security/database-sqli.go b/go/aws-lambda/security/database-sqli.go new file mode 100644 index 00000000..dcabc8db --- /dev/null +++ b/go/aws-lambda/security/database-sqli.go @@ -0,0 +1,120 @@ +package main + +import ( + "database/sql" + "encoding/json" + "log" + "os" + "context" + + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambda" + _ "github.com/go-sql-driver/mysql" +) + +var ( + db *sql.DB + err error + connectionString string + dbUser string + dbPass string + dataSource string +) + +type Employee struct { + EmployeeNo int `json:"emp_no"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` +} + +func init() { + connectionString = os.Getenv("CONN") + dbUser = os.Getenv("DBUSER") + dbPass = os.Getenv("DBPASS") + dataSource = dbUser + ":" + dbPass + "@tcp(" + connectionString + ")/employees" +} + +func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { + + searchCriteria := request.Body + + db, err = sql.Open("mysql", dataSource) + if err != nil { + panic(err.Error()) + } + + defer db.Close() + + // ruleid: database-sqli + results, err := db.Query("select e.emp_no, e.first_name, e.last_name " + + "from employees e, departments d, dept_emp de " + + "where de.emp_no = e.emp_no " + + "and de.dept_no = d.dept_no " + + "and d.dept_name = 'Marketing' " + + "and e.last_name LIKE '" + searchCriteria + "%';") + + if err != nil { + log.Fatal(err) + } + defer results.Close() + + // ok: database-sqli + results2, err2 := db.Query("select * from foobar") + + employees := make([]Employee, 0) + + for results.Next() { + var e Employee + + err := results.Scan(&e.EmployeeNo, &e.FirstName, &e.LastName) + if err != nil { + log.Fatal(err) + } + employees = append(employees, e) + } + + data, _ := json.Marshal(employees) + + return events.APIGatewayProxyResponse{ + StatusCode: 200, + Body: string(data), + IsBase64Encoded: false, + }, nil +} + +func HandleRequest(ctx context.Context, name MyEvent) (string, error) { + searchCriteria := context.Smth + + db, err = sql.Open("mysql", dataSource) + if err != nil { + panic(err.Error()) + } + + defer db.Close() + + // ok: database-sqli + results, err := db.Query("select e.emp_no, e.first_name, e.last_name " + + "from employees e, departments d, dept_emp de " + + "where de.emp_no = e.emp_no " + + "and de.dept_no = d.dept_no " + + "and d.dept_name = 'Marketing' " + + "and e.last_name LIKE '" + searchCriteria + "%';") + + if err != nil { + log.Fatal(err) + } + defer results.Close() + + data, _ := json.Marshal(results) + + return events.APIGatewayProxyResponse{ + StatusCode: 200, + Body: string(data), + IsBase64Encoded: false, + }, nil +} + +func main() { + lambda.Start(handler) + lambda.Start(HandleRequest) +} diff --git a/go/aws-lambda/security/database-sqli.yaml b/go/aws-lambda/security/database-sqli.yaml new file mode 100644 index 00000000..4107a43a --- /dev/null +++ b/go/aws-lambda/security/database-sqli.yaml @@ -0,0 +1,62 @@ +rules: +- id: database-sqli + languages: + - go + message: >- + Detected SQL statement that is tainted by `$EVENT` object. This could lead to SQL injection if the + variable is user-controlled + and not properly sanitized. In order to prevent SQL injection, + use parameterized queries or prepared statements instead. + You can use prepared statements with the 'Prepare' and 'PrepareContext' calls. + mode: taint + metadata: + references: + - https://pkg.go.dev/database/sql#DB.Query + category: security + owasp: + - A01:2017 - Injection + - A03:2021 - Injection + cwe: + - "CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')" + technology: + - aws-lambda + - database + - sql + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - vuln + likelihood: HIGH + impact: MEDIUM + confidence: MEDIUM + pattern-sinks: + - patterns: + - focus-metavariable: $QUERY + - pattern-either: + - pattern: $DB.Exec($QUERY,...) + - pattern: $DB.ExecContent($QUERY,...) + - pattern: $DB.Query($QUERY,...) + - pattern: $DB.QueryContext($QUERY,...) + - pattern: $DB.QueryRow($QUERY,...) + - pattern: $DB.QueryRowContext($QUERY,...) + - pattern-inside: | + import "database/sql" + ... + pattern-sources: + - patterns: + - pattern-either: + - pattern-inside: | + func $HANDLER($CTX $CTXTYPE, $EVENT $TYPE, ...) {...} + ... + lambda.Start($HANDLER, ...) + - patterns: + - pattern-inside: | + func $HANDLER($EVENT $TYPE) {...} + ... + lambda.Start($HANDLER, ...) + - pattern-not-inside: | + func $HANDLER($EVENT context.Context) {...} + ... + lambda.Start($HANDLER, ...) + - focus-metavariable: $EVENT + severity: WARNING diff --git a/go/aws-lambda/security/tainted-sql-string.go b/go/aws-lambda/security/tainted-sql-string.go new file mode 100644 index 00000000..b1c6b80e --- /dev/null +++ b/go/aws-lambda/security/tainted-sql-string.go @@ -0,0 +1,101 @@ +package main + +import ( + "database/sql" + "encoding/json" + "log" + "os" + "strconv" + + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambda" + _ "github.com/go-sql-driver/mysql" +) + +var ( + db *sql.DB + err error + connectionString string + dbUser string + dbPass string + dataSource string +) + +type Employee struct { + EmployeeNo int `json:"emp_no"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` +} + +func init() { + connectionString = os.Getenv("CONN") + dbUser = os.Getenv("DBUSER") + dbPass = os.Getenv("DBPASS") + dataSource = dbUser + ":" + dbPass + "@tcp(" + connectionString + ")/employees" +} + +func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { + + searchCriteria := request.Body + + db, err = sql.Open("mysql", dataSource) + if err != nil { + panic(err.Error()) + } + + defer db.Close() + + // ruleid: tainted-sql-string + results, err := db.Query("select e.emp_no, e.first_name, e.last_name " + + "from employees e, departments d, dept_emp de " + + "where e.last_name LIKE '" + searchCriteria + "%';") + + if err != nil { + log.Fatal(err) + } + defer results.Close() + // ruleid: tainted-sql-string + _, err = db.Exec(` + DELETE FROM table WHERE Id = ` + request.Get("Id")) + // ruleid: tainted-sql-string + _, err = db.Exec("DELETE FROM table WHERE Id = " + request.Get("Id")) + + // ok: tainted-sql-string + log.Printf("DELETE FROM table WHERE Id = " + request.Get("Id")) + // ok: tainted-sql-string + _, err = db.Exec(` FAKE + DELETE FROM table WHERE Id = ` + request.Get("Id")) + + idhtml := request.Get("Id") + id, _ := strconv.Atoi(idhtml) + + // ok: tainted-sql-string + _, err = db.Exec("DELETE FROM table WHERE Id = " + id) + + // ok: tainted-sql-string + results2, err2 := db.Query("select * from foobar") + + employees := make([]Employee, 0) + + for results.Next() { + var e Employee + + err := results.Scan(&e.EmployeeNo, &e.FirstName, &e.LastName) + if err != nil { + log.Fatal(err) + } + employees = append(employees, e) + } + + data, _ := json.Marshal(employees) + + return events.APIGatewayProxyResponse{ + StatusCode: 200, + Body: string(data), + IsBase64Encoded: false, + }, nil +} + +func main() { + lambda.Start(handler) +} diff --git a/go/aws-lambda/security/tainted-sql-string.yaml b/go/aws-lambda/security/tainted-sql-string.yaml new file mode 100644 index 00000000..6f82122a --- /dev/null +++ b/go/aws-lambda/security/tainted-sql-string.yaml @@ -0,0 +1,68 @@ +rules: +- id: tainted-sql-string + languages: [go] + severity: ERROR + message: >- + Detected user input used to manually construct a SQL string. This is usually + bad practice because manual construction could accidentally result in a SQL + injection. An attacker could use a SQL injection to steal or modify contents + of the database. Instead, use a parameterized query which is available + by default in most database engines. Alternatively, consider using an + object-relational mapper (ORM) such as Sequelize which will protect your queries. + metadata: + references: + - https://owasp.org/www-community/attacks/SQL_Injection + category: security + owasp: + - A01:2017 - Injection + - A03:2021 - Injection + cwe: + - "CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')" + technology: + - aws-lambda + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - vuln + likelihood: HIGH + impact: MEDIUM + confidence: MEDIUM + mode: taint + pattern-sources: + - patterns: + - pattern-either: + - pattern-inside: | + func $HANDLER($CTX $CTXTYPE, $EVENT $TYPE, ...) {...} + ... + lambda.Start($HANDLER, ...) + - patterns: + - pattern-inside: | + func $HANDLER($EVENT $TYPE) {...} + ... + lambda.Start($HANDLER, ...) + - pattern-not-inside: | + func $HANDLER($EVENT context.Context) {...} + ... + lambda.Start($HANDLER, ...) + - focus-metavariable: $EVENT + pattern-sinks: + - patterns: + - pattern-either: + - patterns: + - pattern: | + "$SQLSTR" + ... + - metavariable-regex: + metavariable: $SQLSTR + regex: (?i)(\s*select|\s*delete|\s*insert|\s*create|\s*update|\s*alter|\s*drop).* + - patterns: + - pattern-either: + - pattern: fmt.Fprintf($F, "$SQLSTR", ...) + - pattern: fmt.Sprintf("$SQLSTR", ...) + - pattern: fmt.Printf("$SQLSTR", ...) + - metavariable-regex: + metavariable: $SQLSTR + regex: \s*(?i)(select|delete|insert|create|update|alter|drop)\b.*%(v|s|q).* + - pattern-not-inside: | + log.$PRINT(...) + pattern-sanitizers: + - pattern: strconv.Atoi(...) diff --git a/go/gorilla/security/audit/handler-assignment-from-multiple-sources.go b/go/gorilla/security/audit/handler-assignment-from-multiple-sources.go new file mode 100644 index 00000000..ddf0e9fc --- /dev/null +++ b/go/gorilla/security/audit/handler-assignment-from-multiple-sources.go @@ -0,0 +1,104 @@ +package main +import ( + "net/http" + "github.com/gorilla/sessions" +) + +type User struct { + user_id int + account_id string +} + + +func ValidateUser(user_id int) bool { + return true +} + +func RetrieveUser(user_id int) User { + return User{user_id, "0000"} +} + +var store = sessions.NewCookieStore([]byte("blah-blah-blah")) + +func MyHandler(w http.ResponseWriter, r *http.Request) { + session, err := store.Get(r, "blah-session") + user_id := session.Values["user_id"] + + if !ValidateUser(user_id) { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + // ruleid: handler-assignment-from-multiple-sources + user_id = r.query.params.user_id + user_obj := RetrieveUser(user_id) + user_obj.account_id = r.query.params.account_id + user_obj.save() +} + +func MyHandlerExplicit(w http.ResponseWriter, r *http.Request) { + session, err := store.Get(r, "blah-session") + var user_id int = session.Values["user_id"].(int) + + if !ValidateUser(user_id) { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + // ruleid: handler-assignment-from-multiple-sources + user_id = r.query.params.user_id + user_obj := RetrieveUser(user_id) + user_obj.account_id = r.query.params.account_id + user_obj.save() +} + +func augment(user_id int, augment_string string) int { + return user_id +} + +func MyHandlerOK(w http.ResponseWriter, r *http.Request) { + // ok: handler-assignment-from-multiple-sources + session, err := store.Get(r, "blah-session") + user_id := session.Values["user_id"] + + if !ValidateUser(user_id) { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + + user_id = augment(user_id, "hello, world") + user_obj := RetrieveUser(user_id) + user_obj.account_id = r.query.params.account_id + user_obj.save() +} + +func (sc *http.serverConn) runHandler(rw *http.responseWriter, req *http.Request, handler func(http.ResponseWriter, *http.Request)) { + // ok: handler-assignment-from-multiple-sources + didPanic := true + defer func() { + rw.rws.stream.cancelCtx() + if didPanic { + e := recover() + sc.writeFrameFromHandler(FrameWriteRequest{ + write: handlerPanicRST{rw.rws.stream.id}, + stream: rw.rws.stream, + }) + // Same as net/http: + if e != nil && e != http.ErrAbortHandler { + const size = 64 << 10 + // ok: handler-assignment-from-multiple-sources + buf := make([]byte, size) + buf = buf[:runtime.Stack(buf, false)] + sc.logf("http2: panic serving %v: %v\n%s", sc.conn.RemoteAddr(), e, buf) + } + return + } + rw.handlerDone() + }() + handler(rw, req) + didPanic = false +} + +func main() { + http.HandleFunc("/account", MyHandler) + + http.ListenAndServe(":8080", nil) +} diff --git a/go/gorilla/security/audit/handler-assignment-from-multiple-sources.yaml b/go/gorilla/security/audit/handler-assignment-from-multiple-sources.yaml new file mode 100644 index 00000000..6661f465 --- /dev/null +++ b/go/gorilla/security/audit/handler-assignment-from-multiple-sources.yaml @@ -0,0 +1,48 @@ +rules: +- id: handler-assignment-from-multiple-sources + metadata: + cwe: + - 'CWE-289: Authentication Bypass by Alternate Name' + category: security + technology: + - gorilla + confidence: MEDIUM + references: + - https://cwe.mitre.org/data/definitions/289.html + subcategory: + - audit + impact: MEDIUM + likelihood: LOW + mode: taint + pattern-sources: + - patterns: + - pattern-inside: | + func $HANDLER(..., $R *http.Request, ...) { + ... + } + - focus-metavariable: $R + - pattern-either: + - pattern: $R.query + pattern-sinks: + - patterns: + - pattern: | + $Y, err := store.Get(...) + ... + $VAR := $Y.Values[...] + ... + $VAR = $R + - focus-metavariable: $R + - patterns: + - pattern: | + $Y, err := store.Get(...) + ... + var $VAR $INT = $Y.Values["..."].($INT) + ... + $VAR = $R + - focus-metavariable: $R + message: >- + Variable $VAR is assigned from two different sources: '$Y' and '$R'. Make sure this is intended, + as this could cause logic bugs if they are treated as they are the same object. + languages: + - go + severity: WARNING diff --git a/go/gorilla/security/audit/session-cookie-missing-httponly.go b/go/gorilla/security/audit/session-cookie-missing-httponly.go new file mode 100644 index 00000000..bed38a41 --- /dev/null +++ b/go/gorilla/security/audit/session-cookie-missing-httponly.go @@ -0,0 +1,94 @@ +// cf. https://github.com/0c34/govwa/blob/139693e56406b5684d2a6ae22c0af90717e149b8/user/session/session.go + +package main + +import ( + "fmt" + "github.com/gorilla/sessions" + "govwa/util/config" + "log" + "net/http" +) + +type Self struct{} + +func New() *Self { + return &Self{} +} + +var store = sessions.NewCookieStore([]byte(config.Cfg.Sessionkey)) + +func (self *Self) SetSession(w http.ResponseWriter, r *http.Request, data map[string]string) { + session, err := store.Get(r, "govwa") + + if err != nil { + log.Println(err.Error()) + } + + // ruleid: session-cookie-missing-httponly + session.Options = &sessions.Options{ + Path: "/", + MaxAge: 3600, + HttpOnly: false, //set to false for xss :) + Secure: true, + } + + session.Values["govwa_session"] = true + + //create new session to store on server side + if data != nil { + for key, value := range data { + session.Values[key] = value + } + } + err = session.Save(r, w) //safe session and send it to client as cookie + + if err != nil { + log.Println(err.Error()) + } +} + +func (self *Self) GetSession(r *http.Request, key string) string { + session, err := store.Get(r, "govwa") + + if err != nil { + log.Println(err.Error()) + return "" + } + data := session.Values[key] + sv := fmt.Sprintf("%v", data) + return sv +} + +func (self *Self) DeleteSession(w http.ResponseWriter, r *http.Request) { + session, err := store.Get(r, "govwa") + if err != nil { + log.Println(err.Error()) + } + + // ruleid: session-cookie-missing-httponly + session.Options = &sessions.Options{ + MaxAge: -1, + HttpOnly: false, //set to false for xss :) + } + + session.Values["govwa_session"] = false + err = session.Save(r, w) //safe session and send it to client as cookie + + if err != nil { + log.Println(err.Error()) + } + + return +} + +func (self *Self) IsLoggedIn(r *http.Request) bool { + s, err := store.Get(r, "govwa") + if err != nil { + log.Println(err.Error()) + } + if auth, ok := s.Values["govwa_session"].(bool); !ok || !auth { + return false + } + return true +} diff --git a/go/gorilla/security/audit/session-cookie-missing-httponly.yaml b/go/gorilla/security/audit/session-cookie-missing-httponly.yaml new file mode 100644 index 00000000..be9df5e6 --- /dev/null +++ b/go/gorilla/security/audit/session-cookie-missing-httponly.yaml @@ -0,0 +1,39 @@ +rules: +- id: session-cookie-missing-httponly + patterns: + - pattern-not-inside: | + &sessions.Options{ + ..., + HttpOnly: true, + ..., + } + - pattern: | + &sessions.Options{ + ..., + } + message: >- + A session cookie was detected without setting the 'HttpOnly' flag. + The 'HttpOnly' flag for cookies instructs the browser to forbid + client-side scripts from reading the cookie which mitigates XSS + attacks. Set the 'HttpOnly' flag by setting 'HttpOnly' to 'true' + in the Options struct. + metadata: + cwe: + - "CWE-1004: Sensitive Cookie Without 'HttpOnly' Flag" + owasp: + - A05:2021 - Security Misconfiguration + references: + - https://github.com/0c34/govwa/blob/139693e56406b5684d2a6ae22c0af90717e149b8/user/session/session.go#L69 + category: security + technology: + - gorilla + confidence: MEDIUM + subcategory: + - audit + likelihood: LOW + impact: LOW + fix-regex: + regex: (HttpOnly\s*:\s+)false + replacement: \1true + severity: WARNING + languages: [go] diff --git a/go/gorilla/security/audit/session-cookie-missing-secure.go b/go/gorilla/security/audit/session-cookie-missing-secure.go new file mode 100644 index 00000000..fd2c6242 --- /dev/null +++ b/go/gorilla/security/audit/session-cookie-missing-secure.go @@ -0,0 +1,94 @@ +// cf. https://github.com/0c34/govwa/blob/139693e56406b5684d2a6ae22c0af90717e149b8/user/session/session.go + +package main + +import ( + "fmt" + "github.com/gorilla/sessions" + "govwa/util/config" + "log" + "net/http" +) + +type Self struct{} + +func New() *Self { + return &Self{} +} + +var store = sessions.NewCookieStore([]byte(config.Cfg.Sessionkey)) + +func (self *Self) SetSession(w http.ResponseWriter, r *http.Request, data map[string]string) { + session, err := store.Get(r, "govwa") + + if err != nil { + log.Println(err.Error()) + } + + // ruleid: session-cookie-missing-secure + session.Options = &sessions.Options{ + Path: "/", + MaxAge: 3600, + HttpOnly: false, //set to false for xss :) + Secure: false, + } + + session.Values["govwa_session"] = true + + //create new session to store on server side + if data != nil { + for key, value := range data { + session.Values[key] = value + } + } + err = session.Save(r, w) //safe session and send it to client as cookie + + if err != nil { + log.Println(err.Error()) + } +} + +func (self *Self) GetSession(r *http.Request, key string) string { + session, err := store.Get(r, "govwa") + + if err != nil { + log.Println(err.Error()) + return "" + } + data := session.Values[key] + sv := fmt.Sprintf("%v", data) + return sv +} + +func (self *Self) DeleteSession(w http.ResponseWriter, r *http.Request) { + session, err := store.Get(r, "govwa") + if err != nil { + log.Println(err.Error()) + } + + // ruleid: session-cookie-missing-secure + session.Options = &sessions.Options{ + MaxAge: -1, + HttpOnly: false, //set to false for xss :) + } + + session.Values["govwa_session"] = false + err = session.Save(r, w) //safe session and send it to client as cookie + + if err != nil { + log.Println(err.Error()) + } + + return +} + +func (self *Self) IsLoggedIn(r *http.Request) bool { + s, err := store.Get(r, "govwa") + if err != nil { + log.Println(err.Error()) + } + if auth, ok := s.Values["govwa_session"].(bool); !ok || !auth { + return false + } + return true +} diff --git a/go/gorilla/security/audit/session-cookie-missing-secure.yaml b/go/gorilla/security/audit/session-cookie-missing-secure.yaml new file mode 100644 index 00000000..c696e563 --- /dev/null +++ b/go/gorilla/security/audit/session-cookie-missing-secure.yaml @@ -0,0 +1,38 @@ +rules: +- id: session-cookie-missing-secure + patterns: + - pattern-not-inside: | + &sessions.Options{ + ..., + Secure: true, + ..., + } + - pattern: | + &sessions.Options{ + ..., + } + message: >- + A session cookie was detected without setting the 'Secure' flag. + The 'secure' flag for cookies prevents the client from transmitting + the cookie over insecure channels such as HTTP. Set the 'Secure' + flag by setting 'Secure' to 'true' in the Options struct. + metadata: + cwe: + - "CWE-614: Sensitive Cookie in HTTPS Session Without 'Secure' Attribute" + owasp: + - A05:2021 - Security Misconfiguration + references: + - https://github.com/0c34/govwa/blob/139693e56406b5684d2a6ae22c0af90717e149b8/user/session/session.go#L69 + category: security + technology: + - gorilla + confidence: MEDIUM + subcategory: + - audit + likelihood: LOW + impact: LOW + fix-regex: + regex: (Secure\s*:\s+)false + replacement: \1true + severity: WARNING + languages: [go] \ No newline at end of file diff --git a/go/gorilla/security/audit/session-cookie-samesitenone.go b/go/gorilla/security/audit/session-cookie-samesitenone.go new file mode 100644 index 00000000..56b52c79 --- /dev/null +++ b/go/gorilla/security/audit/session-cookie-samesitenone.go @@ -0,0 +1,40 @@ +package main + +import ( + "net/http" + "github.com/gorilla/sessions" +) + +var store = sessions.NewCookieStore([]byte("")) + +func setSessionWithSameSiteNone(w http.ResponseWriter, r *http.Request) { + session, _ := store.Get(r, "session-name") + // ruleid: session-cookie-samesitenone + session.Options = &sessions.Options{ + Path: "/", + MaxAge: 3600, + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteNoneMode, + } + session.Save(r, w) +} + +func setSessionWithSameSiteStrict(w http.ResponseWriter, r *http.Request) { + session, _ := store.Get(r, "session-name") + // ok: session-cookie-samesitenone + session.Options = &sessions.Options{ + Path: "/", + MaxAge: 3600, + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteStrictMode, + } + session.Save(r, w) +} + +func main() { + http.HandleFunc("/set-none", setSessionWithSameSiteNone) + http.HandleFunc("/set-strict", setSessionWithSameSiteStrict) + http.ListenAndServe(":8080", nil) +} diff --git a/go/gorilla/security/audit/session-cookie-samesitenone.yaml b/go/gorilla/security/audit/session-cookie-samesitenone.yaml new file mode 100644 index 00000000..bcec8599 --- /dev/null +++ b/go/gorilla/security/audit/session-cookie-samesitenone.yaml @@ -0,0 +1,36 @@ +rules: +- id: session-cookie-samesitenone + patterns: + - pattern-inside: | + &sessions.Options{ + ..., + SameSite: http.SameSiteNoneMode, + ..., + } + - pattern: | + &sessions.Options{ + ..., + } + message: Found SameSiteNoneMode setting in Gorilla session options. Consider setting + SameSite to Lax, Strict or Default for enhanced security. + metadata: + cwe: + - 'CWE-1275: Sensitive Cookie with Improper SameSite Attribute' + owasp: + - A05:2021 - Security Misconfiguration + references: + - https://pkg.go.dev/github.com/gorilla/sessions#Options + category: security + technology: + - gorilla + confidence: MEDIUM + subcategory: + - audit + likelihood: LOW + impact: LOW + fix-regex: + regex: (SameSite\s*:\s+)http.SameSiteNoneMode + replacement: \1http.SameSiteDefaultMode + severity: WARNING + languages: + - go diff --git a/go/gorilla/security/audit/websocket-missing-origin-check.go b/go/gorilla/security/audit/websocket-missing-origin-check.go new file mode 100644 index 00000000..c8378ec3 --- /dev/null +++ b/go/gorilla/security/audit/websocket-missing-origin-check.go @@ -0,0 +1,49 @@ +package main + +import ( + "log" + "net/http" + + "github.com/gorilla/websocket" +) + +var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, + ReadBufferSize: 1024, + WriteBufferSize: 1024, +} + +var upgrader2 = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, +} + +func handler_check_origin(w http.ResponseWriter, r *http.Request) { + // ok: websocket-missing-origin-check + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Println(err) + return + } +} + +func handler_check_origin2(w http.ResponseWriter, r *http.Request) { + upgrader2.CheckOrigin = func(r *http.Request) bool { return true } + // ok: websocket-missing-origin-check + conn, err := upgrader2.Upgrade(w, r, nil) + if err != nil { + log.Println(err) + return + } +} + +func handler_doesnt_check_origin(w http.ResponseWriter, r *http.Request) { + // ruleid: websocket-missing-origin-check + conn, err := upgrader2.Upgrade(w, r, nil) + if err != nil { + log.Println(err) + return + } +} diff --git a/go/gorilla/security/audit/websocket-missing-origin-check.yaml b/go/gorilla/security/audit/websocket-missing-origin-check.yaml new file mode 100644 index 00000000..73a3d0a7 --- /dev/null +++ b/go/gorilla/security/audit/websocket-missing-origin-check.yaml @@ -0,0 +1,39 @@ +rules: +- id: websocket-missing-origin-check + patterns: + - pattern-inside: | + import ("github.com/gorilla/websocket") + ... + - patterns: + - pattern-not-inside: | + $UPGRADER = websocket.Upgrader{..., CheckOrigin: $FN ,...} + ... + - pattern-not-inside: | + $UPGRADER.CheckOrigin = $FN2 + ... + - pattern: | + $UPGRADER.Upgrade(...) + message: >- + The Origin header in the HTTP WebSocket handshake is used to guarantee that the + connection accepted by the WebSocket is from a trusted origin domain. Failure to enforce can + lead to Cross Site Request Forgery (CSRF). As per "gorilla/websocket" documentation: "A CheckOrigin function + should carefully validate the request origin to prevent cross-site request forgery." + languages: [go] + severity: WARNING + metadata: + category: security + cwe: + - 'CWE-352: Cross-Site Request Forgery (CSRF)' + owasp: + - A01:2021 - Broken Access Control + references: + - https://pkg.go.dev/github.com/gorilla/websocket#Upgrader + technology: + - gorilla + confidence: MEDIUM + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - audit + likelihood: LOW + impact: LOW diff --git a/go/gorm/security/audit/gorm-dangerous-methods-usage.go b/go/gorm/security/audit/gorm-dangerous-methods-usage.go new file mode 100644 index 00000000..5401b921 --- /dev/null +++ b/go/gorm/security/audit/gorm-dangerous-methods-usage.go @@ -0,0 +1,81 @@ +package main + +import ( + "fmt" + "net/http" + + "github.com/gorilla/mux" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +type User struct { + gorm.Model + FirstName string + LastName string + Email string `gorm:"unique_index:user_email_index"` + Password string + Token string + TokenExpiresAt uint +} + +func testInjection(w http.ResponseWriter, r *http.Request, db *gorm.DB) { + param := r.Cookie("foo") + if param != "" { + table := db.Table("users") + var u User + //ruleid: gorm-dangerous-method-usage + table.Order(param).Find(&u) + + } +} + +func testInjection2(w http.ResponseWriter, r *http.Request, db *gorm.DB) { + param := r.URL.Query().Get("orderBy") + if param != "" { + table := db.Table("users") + var u User + //ruleid: gorm-dangerous-method-usage + table.Order(param + " " + "ASC").Find(&u) + } +} + +func testNoInjection(w http.ResponseWriter, r *http.Request, db *gorm.DB) { + table := db.Table("users") + var u User + //ok: gorm-dangerous-method-usage + table.Order("email").Find(&u) +} + +func testNoInjection2(w http.ResponseWriter, r *http.Request, db *gorm.DB) { + table := db.Table("users") + var orderBy = "email" + var u User + //ok: gorm-dangerous-method-usage + table.Order(orderBy).Find(&u) +} + +func testNoInjection3(w http.ResponseWriter, r *http.Request, db *gorm.DB) { + param := r.URL.Query().Get("orderBy") + if param != "" { + table := db.Table("users") + var u User + //ok: gorm-dangerous-method-usage + table.Order((param != "param") + " " + "ASC").Find(&u) + } +} + +func main() { + dsn := "dbuser:password@tcp(127.0.0.1:3306)/users?charset=utf8&parseTime=True" + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) + if err != nil { + fmt.Println(err) + } + db.AutoMigrate(&User{}) + myRouter := mux.NewRouter().StrictSlash(true) + myRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + testInjection(w, r, db) + }).Methods("GET") + http.ListenAndServe(":10000", myRouter) + +} \ No newline at end of file diff --git a/go/gorm/security/audit/gorm-dangerous-methods-usage.yaml b/go/gorm/security/audit/gorm-dangerous-methods-usage.yaml new file mode 100644 index 00000000..2dbf73f6 --- /dev/null +++ b/go/gorm/security/audit/gorm-dangerous-methods-usage.yaml @@ -0,0 +1,69 @@ +rules: +- id: gorm-dangerous-method-usage + message: >- + Detected usage of dangerous method $METHOD which does not escape inputs (see link in references). + If the argument is user-controlled, this can lead to SQL injection. When using $METHOD function, + do not trust user-submitted data and only allow approved list of input (possibly, use an allowlist + approach). + severity: WARNING + languages: + - go + mode: taint + pattern-sources: + - patterns: + - pattern-either: + - pattern: | + ($REQUEST : http.Request).$ANYTHING + - pattern: | + ($REQUEST : *http.Request).$ANYTHING + - metavariable-regex: + metavariable: $ANYTHING + regex: ^(BasicAuth|Body|Cookie|Cookies|Form|FormValue|GetBody|Host|MultipartReader|ParseForm|ParseMultipartForm|PostForm|PostFormValue|Referer|RequestURI|Trailer|TransferEncoding|UserAgent|URL)$ + pattern-sinks: + - patterns: + - pattern-inside: | + import ("gorm.io/gorm") + ... + - patterns: + - pattern-inside: | + func $VAL(..., $GORM *gorm.DB,... ) { + ... + } + - pattern-either: + - pattern: | + $GORM. ... .$METHOD($VALUE) + - pattern: | + $DB := $GORM. ... .$ANYTHING(...) + ... + $DB. ... .$METHOD($VALUE) + - focus-metavariable: $VALUE + - metavariable-regex: + metavariable: $METHOD + regex: ^(Order|Exec|Raw|Group|Having|Distinct|Select|Pluck)$ + pattern-sanitizers: + - pattern-either: + - pattern: strconv.Atoi(...) + - pattern: | + ($X: bool) + options: + interfile: true + metadata: + category: security + technology: + - gorm + cwe: + - "CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')" + owasp: + - A01:2017 - Injection + - A03:2021 - Injection + references: + - https://gorm.io/docs/security.html#SQL-injection-Methods + - https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html + confidence: HIGH + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - vuln + likelihood: HIGH + impact: MEDIUM + interfile: true diff --git a/go/grpc/security/grpc-client-insecure-connection.go b/go/grpc/security/grpc-client-insecure-connection.go new file mode 100644 index 00000000..70c9eeb3 --- /dev/null +++ b/go/grpc/security/grpc-client-insecure-connection.go @@ -0,0 +1,24 @@ +package insecuregrpc + +import ( + "google.golang.org/grpc" +) + +// cf. https://blog.gopheracademy.com/advent-2019/go-grps-and-tls/#connection-without-encryption +func unsafe() { + // ruleid:grpc-client-insecure-connection + conn, err := grpc.Dial(address, grpc.WithInsecure()) + if err != nil { + log.Fatalf("did not connect: %v", err) + } + defer conn.Close() +} + +func safe() { + // ok:grpc-client-insecure-connection + conn, err := grpc.Dial(address) + if err != nil { + log.Fatalf("did not connect: %v", err) + } + defer conn.Close() +} diff --git a/go/grpc/security/grpc-client-insecure-connection.yaml b/go/grpc/security/grpc-client-insecure-connection.yaml new file mode 100644 index 00000000..9c453006 --- /dev/null +++ b/go/grpc/security/grpc-client-insecure-connection.yaml @@ -0,0 +1,33 @@ +rules: +- id: grpc-client-insecure-connection + metadata: + cwe: + - 'CWE-300: Channel Accessible by Non-Endpoint' + references: + - https://blog.gopheracademy.com/advent-2019/go-grps-and-tls/#connection-without-encryption + category: security + technology: + - grpc + confidence: HIGH + owasp: + - A07:2021 - Identification and Authentication Failures + subcategory: + - audit + likelihood: LOW + impact: LOW + message: >- + Found an insecure gRPC connection using 'grpc.WithInsecure()'. This creates a + connection without encryption to a gRPC + server. A malicious attacker could tamper with the gRPC message, which could compromise + the machine. Instead, establish + a secure connection with an + SSL certificate using the 'grpc.WithTransportCredentials()' function. You can + create a create credentials using a 'tls.Config{}' + struct with 'credentials.NewTLS()'. The final fix looks like this: 'grpc.WithTransportCredentials(credentials.NewTLS())'. + languages: + - go + severity: ERROR + pattern: $GRPC.Dial($ADDR, ..., $GRPC.WithInsecure(...), ...) + fix-regex: + regex: (.*)WithInsecure\(.*?\) + replacement: \1WithTransportCredentials(credentials.NewTLS()) diff --git a/go/grpc/security/grpc-server-insecure-connection.go b/go/grpc/security/grpc-server-insecure-connection.go new file mode 100644 index 00000000..aeb45aed --- /dev/null +++ b/go/grpc/security/grpc-server-insecure-connection.go @@ -0,0 +1,89 @@ +package insecuregrpc + +import ( + "crypto/x509" + "log" + "net/http" + "net/http/httptest" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +// cf. https://blog.gopheracademy.com/advent-2019/go-grps-and-tls/#connection-without-encryption +func unsafe() { + // Server + // ruleid:grpc-server-insecure-connection + s := grpc.NewServer() + // ... register gRPC services ... + if err = s.Serve(lis); err != nil { + log.Fatalf("failed to serve: %v", err) + } +} + +func safe() { + // Server + // ok:grpc-server-insecure-connection + s := grpc.NewServer(grpc.Creds(credentials.NewClientTLSFromCert(x509.NewCertPool(), ""))) + // ... register gRPC services ... + if err = s.Serve(lis); err != nil { + log.Fatalf("failed to serve: %v", err) + } +} + +// False Positive test +// cf. https://github.com/daghan/invoicer-chapter2/blob/4c5b00408a4aeece86d98ad3ef1c88e610053dfc/vendor/golang.org/x/net/websocket/websocket_test.go#L129 +func startServer() { + http.Handle("/echo", Handler(echoServer)) + http.Handle("/count", Handler(countServer)) + http.Handle("/ctrldata", Handler(ctrlAndDataServer)) + subproto := Server{ + Handshake: subProtocolHandshake, + Handler: Handler(subProtoServer), + } + http.Handle("/subproto", subproto) + // ok:grpc-server-insecure-connection + server := httptest.NewServer(nil) + serverAddr = server.Listener.Addr().String() + log.Print("Test WebSocket server listening on ", serverAddr) +} + +// False Positive test - options have grpc.Creds +func startServerWithOpts() { + options := []grpc.ServerOption{ + grpc.Creds(credentials.NewClientTLSFromCert(pool, addr)), + } + // ok:grpc-server-insecure-connection + grpcServer := grpc.NewServer(options...) + _ = grpcServer +} + +// False Positive test - options have grpc.Creds, credentials in a variable +func startServerCredsVar() { + creds := credentials.NewClientTLSFromCert(xpool, xaddr) + options := []grpc.ServerOption{ + grpc.Creds(creds), + grpc.UnaryInterceptor(auth.GRPCInterceptor), + } + // ok:grpc-server-insecure-connection + grpcServer := grpc.NewServer(options...) + _ = grpcServer +} + +func startServerWithOtherCreds() { + creds := credentials.NewTLS(tlsConfig) + logger := penglog.GlobalLogger() + logInterceptor := penggrpc.NewAccessLogInterceptor(&logger, grpcLogFields) + opts := []grpc.ServerOption{ + grpc.Creds(creds), + grpc.ChainUnaryInterceptor( + logInterceptor.UnaryServerInterceptor, + auth.GRPCInterceptor, + ), + grpc.MaxRecvMsgSize(maxRecvMsgSize), + } + // ok:grpc-server-insecure-connection + grpcServer := grpc.NewServer(opts) + _ = grpcServer +} + diff --git a/go/grpc/security/grpc-server-insecure-connection.yaml b/go/grpc/security/grpc-server-insecure-connection.yaml new file mode 100644 index 00000000..c5d6c3e7 --- /dev/null +++ b/go/grpc/security/grpc-server-insecure-connection.yaml @@ -0,0 +1,43 @@ +rules: +- id: grpc-server-insecure-connection + metadata: + cwe: + - 'CWE-300: Channel Accessible by Non-Endpoint' + references: + - https://blog.gopheracademy.com/advent-2019/go-grps-and-tls/#connection-without-encryption + category: security + technology: + - grpc + confidence: HIGH + owasp: + - A07:2021 - Identification and Authentication Failures + subcategory: + - audit + likelihood: LOW + impact: LOW + message: >- + Found an insecure gRPC server without 'grpc.Creds()' or options with credentials. + This allows for a connection without + encryption to this server. + A malicious attacker could tamper with the gRPC message, which could compromise + the machine. Include credentials derived + from an SSL certificate in order to create a secure gRPC connection. You can create + credentials using 'credentials.NewServerTLSFromFile("cert.pem", + "cert.key")'. + languages: + - go + severity: ERROR + mode: taint + pattern-sinks: + - requires: OPTIONS and not CREDS + pattern: grpc.NewServer($OPT, ...) + - requires: EMPTY_CONSTRUCTOR + pattern: grpc.NewServer() + pattern-sources: + - label: OPTIONS + pattern: grpc.ServerOption{ ... } + - label: CREDS + pattern: grpc.Creds(...) + - label: EMPTY_CONSTRUCTOR + pattern: grpc.NewServer() + diff --git a/go/jwt-go/security/audit/jwt-parse-unverified.go b/go/jwt-go/security/audit/jwt-parse-unverified.go new file mode 100644 index 00000000..ff68b39a --- /dev/null +++ b/go/jwt-go/security/audit/jwt-parse-unverified.go @@ -0,0 +1,37 @@ +package main + +import ( + "fmt" + + "github.com/dgrijalva/jwt-go" +) + +func bad1(tokenString string) { + // ruleid: jwt-go-parse-unverified + token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{}) + if err != nil { + fmt.Println(err) + return + } + + if claims, ok := token.Claims.(jwt.MapClaims); ok { + fmt.Println(claims["foo"], claims["exp"]) + } else { + fmt.Println(err) + } +} + +func ok1(tokenString string, keyFunc Keyfunc) { + // ok: jwt-go-parse-unverified + token, err := new(jwt.Parser).ParseWithClaims(tokenString, jwt.MapClaims{}, keyFunc) + if err != nil { + fmt.Println(err) + return + } + + if claims, ok := token.Claims.(jwt.MapClaims); ok { + fmt.Println(claims["foo"], claims["exp"]) + } else { + fmt.Println(err) + } +} diff --git a/go/jwt-go/security/audit/jwt-parse-unverified.yaml b/go/jwt-go/security/audit/jwt-parse-unverified.yaml new file mode 100644 index 00000000..5916496e --- /dev/null +++ b/go/jwt-go/security/audit/jwt-parse-unverified.yaml @@ -0,0 +1,32 @@ +rules: +- id: jwt-go-parse-unverified + message: >- + Detected the decoding of a JWT token without a verify step. + Don't use `ParseUnverified` unless you know what you're doing + This method parses the token but doesn't validate the signature. It's only ever useful in cases where + you know the signature is valid (because it has been checked previously in the stack) and you want + to extract values from it. + metadata: + cwe: + - 'CWE-345: Insufficient Verification of Data Authenticity' + owasp: + - A08:2021 - Software and Data Integrity Failures + source-rule-url: https://semgrep.dev/blog/2020/hardcoded-secrets-unverified-tokens-and-other-common-jwt-mistakes/ + category: security + technology: + - jwt + confidence: MEDIUM + references: + - https://owasp.org/Top10/A08_2021-Software_and_Data_Integrity_Failures + subcategory: + - audit + likelihood: LOW + impact: LOW + languages: [go] + severity: WARNING + patterns: + - pattern-inside: | + import "github.com/dgrijalva/jwt-go" + ... + - pattern: | + $JWT.ParseUnverified(...) diff --git a/go/jwt-go/security/jwt-none-alg.go b/go/jwt-go/security/jwt-none-alg.go new file mode 100644 index 00000000..2da109b3 --- /dev/null +++ b/go/jwt-go/security/jwt-none-alg.go @@ -0,0 +1,31 @@ +package main + +import ( + "fmt" + "github.com/dgrijalva/jwt-go" +) + +func bad1() { + claims := jwt.StandardClaims{ + ExpiresAt: 15000, + Issuer: "test", + } + + // ruleid: jwt-go-none-algorithm + token := jwt.NewWithClaims(jwt.SigningMethodNone, claims) + // ruleid: jwt-go-none-algorithm + ss, err := token.SignedString(jwt.UnsafeAllowNoneSignatureType) + fmt.Printf("%v %v\n", ss, err) +} + +func ok1(key []byte) { + claims := jwt.StandardClaims{ + ExpiresAt: 15000, + Issuer: "test", + } + + // ok: jwt-go-none-algorithm + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + ss, err := token.SignedString(key) + fmt.Printf("%v %v\n", ss, err) +} diff --git a/go/jwt-go/security/jwt-none-alg.yaml b/go/jwt-go/security/jwt-none-alg.yaml new file mode 100644 index 00000000..f99e91dd --- /dev/null +++ b/go/jwt-go/security/jwt-none-alg.yaml @@ -0,0 +1,39 @@ +rules: +- id: jwt-go-none-algorithm + message: >- + Detected use of the 'none' algorithm in a JWT token. + The 'none' algorithm assumes the integrity of the token has already + been verified. This would allow a malicious actor to forge a JWT token + that will automatically be verified. Do not explicitly use the 'none' + algorithm. Instead, use an algorithm such as 'HS256'. + metadata: + cwe: + - 'CWE-327: Use of a Broken or Risky Cryptographic Algorithm' + owasp: + - A03:2017 - Sensitive Data Exposure + - A02:2021 - Cryptographic Failures + source-rule-url: https://semgrep.dev/blog/2020/hardcoded-secrets-unverified-tokens-and-other-common-jwt-mistakes/ + category: security + technology: + - jwt + confidence: HIGH + references: + - https://owasp.org/Top10/A02_2021-Cryptographic_Failures + subcategory: + - audit + likelihood: LOW + impact: LOW + languages: [go] + severity: ERROR + patterns: + - pattern-either: + - pattern-inside: | + import "github.com/golang-jwt/jwt" + ... + - pattern-inside: | + import "github.com/dgrijalva/jwt-go" + ... + - pattern-either: + - pattern: | + jwt.SigningMethodNone + - pattern: jwt.UnsafeAllowNoneSignatureType diff --git a/go/jwt-go/security/jwt.go b/go/jwt-go/security/jwt.go new file mode 100644 index 00000000..3959feba --- /dev/null +++ b/go/jwt-go/security/jwt.go @@ -0,0 +1,96 @@ +// https://www.sohamkamani.com/blog/golang/2019-01-01-jwt-authentication/ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/dgrijalva/jwt-go" +) + +//... +// import the jwt-go library + +//... + +var users = map[string]string{ + "user1": "password1", + "user2": "password2", +} + +// Create a struct to read the username and password from the request body +type Credentials struct { + Password string `json:"password"` + Username string `json:"username"` +} + +// Create a struct that will be encoded to a JWT. +// We add jwt.StandardClaims as an embedded type, to provide fields like expiry time +type Claims struct { + Username string `json:"username"` + jwt.StandardClaims +} + +// Create the Signin handler +func Signin(w http.ResponseWriter, r *http.Request) { + + // Create the JWT key used to create the signature + var jwtKey = []byte("my_secret_key") + var x = "foo" + + var creds Credentials + // Get the JSON body and decode into credentials + err := json.NewDecoder(r.Body).Decode(&creds) + if err != nil { + // If the structure of the body is wrong, return an HTTP error + w.WriteHeader(http.StatusBadRequest) + return + } + + // Get the expected password from our in memory map + expectedPassword, ok := users[creds.Username] + + // If a password exists for the given user + // AND, if it is the same as the password we received, the we can move ahead + // if NOT, then we return an "Unauthorized" status + if !ok || expectedPassword != creds.Password { + w.WriteHeader(http.StatusUnauthorized) + return + } + + // Declare the expiration time of the token + // here, we have kept it as 5 minutes + expirationTime := time.Now().Add(5 * time.Minute) + // Create the JWT claims, which includes the username and expiry time + claims := &Claims{ + Username: creds.Username, + StandardClaims: jwt.StandardClaims{ + // In JWT, the expiry time is expressed as unix milliseconds + ExpiresAt: expirationTime.Unix(), + }, + } + + // Declare the token with the algorithm used for signing, and the claims + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + // ruleid: hardcoded-jwt-key + tokenString, err := token.SignedString(jwtKey) + // ruleid: hardcoded-jwt-key + tokenString, err := token.SignedString([]byte("my_secret_key")) + if err != nil { + // If there is an error in creating the JWT return an internal server error + w.WriteHeader(http.StatusInternalServerError) + return + } + + // Finally, we set the client cookie for "token" as the JWT we just generated + // we also set an expiry time which is the same as the token itself + http.SetCookie(w, &http.Cookie{ + Name: "token", + Value: tokenString, + Expires: expirationTime, + }) + + +} diff --git a/go/jwt-go/security/jwt.yaml b/go/jwt-go/security/jwt.yaml new file mode 100644 index 00000000..261316b7 --- /dev/null +++ b/go/jwt-go/security/jwt.yaml @@ -0,0 +1,42 @@ +rules: +- id: hardcoded-jwt-key + message: >- + A hard-coded credential was detected. It is not recommended to store credentials in source-code, + as this risks secrets + being leaked and used by either an internal or external malicious adversary. It is recommended to + use environment variables to securely provide credentials or retrieve credentials from a secure + vault or HSM (Hardware Security Module). + options: + interfile: true + metadata: + cwe: + - 'CWE-798: Use of Hard-coded Credentials' + references: + - https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html + owasp: + - A07:2021 - Identification and Authentication Failures + category: security + technology: + - jwt + - secrets + confidence: MEDIUM + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - vuln + likelihood: HIGH + impact: MEDIUM + interfile: true + severity: WARNING + languages: [go] + mode: taint + pattern-sources: + - patterns: + - pattern-inside: | + []byte("$F") + pattern-sinks: + - patterns: + - pattern-either: + - pattern-inside: | + $TOKEN.SignedString($F) + - focus-metavariable: $F diff --git a/go/lang/best-practice/channel-guarded-with-mutex.go b/go/lang/best-practice/channel-guarded-with-mutex.go new file mode 100644 index 00000000..2eefa857 --- /dev/null +++ b/go/lang/best-practice/channel-guarded-with-mutex.go @@ -0,0 +1,33 @@ +package main + +import ( + "fmt" + "sync" +) + +func ReadMessage() { + messages := make(chan string) + + go func() { + messages <- "ping" + }() + + // ok: channel-guarded-with-mutex + msg := <-messages + fmt.Println(msg) +} + +func ReadMessageMutex() { + var mutex = &sync.Mutex{} + messages := make(chan string) + + go func() { + messages <- "ping" + }() + + // ruleid: channel-guarded-with-mutex + mutex.Lock() + msg := <-messages + mutex.Unlock() + fmt.Println(msg) +} diff --git a/go/lang/best-practice/channel-guarded-with-mutex.yaml b/go/lang/best-practice/channel-guarded-with-mutex.yaml new file mode 100644 index 00000000..ab610954 --- /dev/null +++ b/go/lang/best-practice/channel-guarded-with-mutex.yaml @@ -0,0 +1,22 @@ +rules: + - id: channel-guarded-with-mutex + pattern-either: + - pattern: | + $MUX.Lock() + $VALUE <- $CHANNEL + $MUX.Unlock() + - pattern: | + $MUX.Lock() + $VALUE = <- $CHANNEL + $MUX.Unlock() + message: >- + Detected a channel guarded with a mutex. Channels already have + an internal mutex, so this is unnecessary. Remove the mutex. + See https://hackmongo.com/page/golang-antipatterns/#guarded-channel + for more information. + languages: [go] + severity: WARNING + metadata: + category: best-practice + technology: + - go diff --git a/go/lang/best-practice/hidden-goroutine.go b/go/lang/best-practice/hidden-goroutine.go new file mode 100644 index 00000000..9019c9d0 --- /dev/null +++ b/go/lang/best-practice/hidden-goroutine.go @@ -0,0 +1,26 @@ +package main + +import "fmt" + +// ruleid: hidden-goroutine +func HiddenGoroutine() { + go func() { + fmt.Println("hello world") + }() +} + +// ok: hidden-goroutine +func FunctionThatCallsGoroutineIsOk() { + fmt.Println("This is normal") + go func() { + fmt.Println("This is OK because the function does other things") + }() +} + +// ok: hidden-goroutine +func FunctionThatCallsGoroutineAlsoOk() { + go func() { + fmt.Println("This is OK because the function does other things") + }() + fmt.Println("This is normal") +} diff --git a/go/lang/best-practice/hidden-goroutine.yaml b/go/lang/best-practice/hidden-goroutine.yaml new file mode 100644 index 00000000..16425cca --- /dev/null +++ b/go/lang/best-practice/hidden-goroutine.yaml @@ -0,0 +1,27 @@ +rules: + - id: hidden-goroutine + patterns: + - pattern-not: | + func $FUNC(...) { + go func() { + ... + }(...) + $MORE + } + - pattern: | + func $FUNC(...) { + go func() { + ... + }(...) + } + message: >- + Detected a hidden goroutine. Function invocations are expected to synchronous, + and this function will execute asynchronously because all it does is call a + goroutine. Instead, remove the internal goroutine and call the function using + 'go'. + languages: [go] + severity: WARNING + metadata: + category: best-practice + technology: + - go diff --git a/go/lang/correctness/dos/zip_bomb.go b/go/lang/correctness/dos/zip_bomb.go new file mode 100644 index 00000000..86bdcd12 --- /dev/null +++ b/go/lang/correctness/dos/zip_bomb.go @@ -0,0 +1,38 @@ +package main + +import ( + "archive/zip" + "io" + "os" + "strconv" +) + +func main() { + // ruleid: potential-dos-via-decompression-bomb + r, err := zip.OpenReader("tmp.zip") + if err != nil { + panic(err) + } + defer r.Close() + + for i, f := range r.File { + out, err := os.OpenFile("output"+strconv.Itoa(i), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + panic(err) + } + + rc, err := f.Open() + if err != nil { + panic(err) + } + + _, err = io.Copy(out, rc) + + out.Close() + rc.Close() + + if err != nil { + panic(err) + } + } +} diff --git a/go/lang/correctness/dos/zlib_bomb.go b/go/lang/correctness/dos/zlib_bomb.go new file mode 100644 index 00000000..8d5b612d --- /dev/null +++ b/go/lang/correctness/dos/zlib_bomb.go @@ -0,0 +1,22 @@ +package main + +import ( + "bytes" + "compress/zlib" + "io" + "os" +) + +func main() { + buff := []byte{120, 156, 202, 72, 205, 201, 201, 215, 81, 40, 207, + 47, 202, 73, 225, 2, 4, 0, 0, 255, 255, 33, 231, 4, 147} + b := bytes.NewReader(buff) + // ruleid: potential-dos-via-decompression-bomb + r, err := zlib.NewReader(b) + if err != nil { + panic(err) + } + io.Copy(os.Stdout, r) + + r.Close() +} diff --git a/go/lang/correctness/looppointer.go b/go/lang/correctness/looppointer.go new file mode 100644 index 00000000..5d165d50 --- /dev/null +++ b/go/lang/correctness/looppointer.go @@ -0,0 +1,39 @@ +func() { + values := []string{"a", "b", "c"} + var funcs []func() + // ruleid:exported_loop_pointer + for _, val := range values { + funcs = append(funcs, func() { + fmt.Println(&val) + }) + } +} + +func() { + // ruleid:exported_loop_pointer + for _, val := range values { + print_pointer(&val) + } +} + + +func() { + values := []string{"a", "b", "c"} + var funcs []func() + // ok:exported_loop_pointer + for _, val := range values { + val := val // pin! + funcs = append(funcs, func() { + fmt.Println(&val) + }) + } +} + +func (){ + input := []string{"a", "b", "c"} + output := []string{} + // ok:exported_loop_pointer + for _, val := range input { + output = append(output, val) + } +} diff --git a/go/lang/correctness/looppointer.yaml b/go/lang/correctness/looppointer.yaml new file mode 100644 index 00000000..61d5139f --- /dev/null +++ b/go/lang/correctness/looppointer.yaml @@ -0,0 +1,29 @@ +rules: + - id: exported_loop_pointer + message: >- + `$VALUE` is a loop pointer that may be exported from the loop. This pointer is + shared between loop iterations, so the exported reference will always point to + the last loop value, which is likely unintentional. To fix, copy the pointer to + a new pointer within the loop. + metadata: + references: + - https://github.com/kyoh86/looppointer + category: correctness + technology: + - go + severity: WARNING + languages: + - go + pattern-either: + - pattern: | + for _, $VALUE := range $SOURCE { + <... &($VALUE) ...> + } + - pattern: | + for _, $VALUE := range $SOURCE { + <... func() { <... &$VALUE ...> } ...> + } + - pattern: | + for _, $VALUE := range $SOURCE { + <... $ANYTHING(..., <... &$VALUE ...>, ...) ...> + } diff --git a/go/lang/correctness/overflow/overflow.go b/go/lang/correctness/overflow/overflow.go new file mode 100644 index 00000000..ea8c432b --- /dev/null +++ b/go/lang/correctness/overflow/overflow.go @@ -0,0 +1,53 @@ +package main + +import ( + "fmt" + "strconv" +) + +func mainInt16Ex1() { + // ruleid: integer-overflow-int16 + bigValue, err := strconv.Atoi("2147483648") + if err != nil { + panic(err) + } + value := int16(bigValue) + fmt.Println(value) +} + +func mainInt16Ex2() { + // ok: integer-overflow-int16 + bigValue, err := strconv.Atoi("10") + if err != nil { + panic(err) + } + value := int16(bigValue) + fmt.Println(value) +} + +func mainInt32Ex1() { + // ruleid: integer-overflow-int32 + bigValue, err := strconv.Atoi("2147483648") + if err != nil { + panic(err) + } + value := int32(bigValue) + fmt.Println(value) +} + +func mainInt32Ex2() { + // ok: integer-overflow-int32 + bigValue, err := strconv.Atoi("10") + if err != nil { + panic(err) + } + value := int32(bigValue) + fmt.Println(value) +} + +func main() { + mainInt16Ex1() + mainInt16Ex2() + mainInt32Ex1() + mainInt32Ex2() +} diff --git a/go/lang/correctness/overflow/overflow.yaml b/go/lang/correctness/overflow/overflow.yaml new file mode 100644 index 00000000..09c753fb --- /dev/null +++ b/go/lang/correctness/overflow/overflow.yaml @@ -0,0 +1,39 @@ +rules: + - id: integer-overflow-int16 + message: + Detected conversion of the result of a strconv.Atoi command to an int16. This could lead to an integer overflow, + which could possibly result in unexpected behavior and even privilege escalation. Instead, use `strconv.ParseInt`. + languages: [go] + severity: WARNING + patterns: + - pattern: | + $F, $ERR := strconv.Atoi($NUM) + ... + int16($F) + - metavariable-comparison: + metavariable: $NUM + comparison: $NUM > 32767 or $NUM < -32768 + strip: true + metadata: + category: correctness + technology: + - go + - id: integer-overflow-int32 + message: + Detected conversion of the result of a strconv.Atoi command to an int32. This could lead to an integer overflow, + which could possibly result in unexpected behavior and even privilege escalation. Instead, use `strconv.ParseInt`. + languages: [go] + severity: WARNING + patterns: + - pattern: | + $F, $ERR := strconv.Atoi($NUM) + ... + int32($F) + - metavariable-comparison: + metavariable: $NUM + comparison: $NUM > 2147483647 or $NUM < -2147483648 + strip: true + metadata: + category: correctness + technology: + - go diff --git a/go/lang/correctness/permissions/file_permission.fixed.go b/go/lang/correctness/permissions/file_permission.fixed.go new file mode 100644 index 00000000..ba86795b --- /dev/null +++ b/go/lang/correctness/permissions/file_permission.fixed.go @@ -0,0 +1,73 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" +) + +func main() { +} + +func test_chmod() { + // ruleid: incorrect-default-permission + err := os.Chmod("/tmp/somefile", 0600) + if err != nil { + fmt.Println("Error when changing file permissions!") + return + } + + // ok: incorrect-default-permission + err := os.Chmod("/tmp/somefile", 0400) + if err != nil { + fmt.Println("Error when changing file permissions!") + return + } +} + +func test_mkdir() { + // ruleid: incorrect-default-permission + err := os.Mkdir("/tmp/mydir", 0600) + if err != nil { + fmt.Println("Error when creating a directory!") + return + } + + // ruleid: incorrect-default-permission + err = os.MkdirAll("/tmp/mydir", 0600) + if err != nil { + fmt.Println("Error when creating a directory!") + return + } + + // ok: incorrect-default-permission + err := os.MkdirAll("/tmp/mydir", 0600) + if err != nil { + fmt.Println("Error when creating a directory!") + return + } +} + +func test_openfile() { + // ruleid: incorrect-default-permission + _, err := os.OpenFile("/tmp/thing", os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + fmt.Println("Error opening a file!") + return + } + + // ok: incorrect-default-permission + _, err := os.OpenFile("/tmp/thing", os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + fmt.Println("Error opening a file!") + return + } +} + +func test_writefile() { + // ruleid: incorrect-default-permission + err := ioutil.WriteFile("/tmp/demo2", []byte("This is some data"), 0600) + if err != nil { + fmt.Println("Error while writing!") + } +} diff --git a/go/lang/correctness/permissions/file_permission.go b/go/lang/correctness/permissions/file_permission.go new file mode 100644 index 00000000..c68c0292 --- /dev/null +++ b/go/lang/correctness/permissions/file_permission.go @@ -0,0 +1,73 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" +) + +func main() { +} + +func test_chmod() { + // ruleid: incorrect-default-permission + err := os.Chmod("/tmp/somefile", 0777) + if err != nil { + fmt.Println("Error when changing file permissions!") + return + } + + // ok: incorrect-default-permission + err := os.Chmod("/tmp/somefile", 0400) + if err != nil { + fmt.Println("Error when changing file permissions!") + return + } +} + +func test_mkdir() { + // ruleid: incorrect-default-permission + err := os.Mkdir("/tmp/mydir", 0777) + if err != nil { + fmt.Println("Error when creating a directory!") + return + } + + // ruleid: incorrect-default-permission + err = os.MkdirAll("/tmp/mydir", 0777) + if err != nil { + fmt.Println("Error when creating a directory!") + return + } + + // ok: incorrect-default-permission + err := os.MkdirAll("/tmp/mydir", 0600) + if err != nil { + fmt.Println("Error when creating a directory!") + return + } +} + +func test_openfile() { + // ruleid: incorrect-default-permission + _, err := os.OpenFile("/tmp/thing", os.O_CREATE|os.O_WRONLY, 0666) + if err != nil { + fmt.Println("Error opening a file!") + return + } + + // ok: incorrect-default-permission + _, err := os.OpenFile("/tmp/thing", os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + fmt.Println("Error opening a file!") + return + } +} + +func test_writefile() { + // ruleid: incorrect-default-permission + err := ioutil.WriteFile("/tmp/demo2", []byte("This is some data"), 0644) + if err != nil { + fmt.Println("Error while writing!") + } +} diff --git a/go/lang/correctness/permissions/file_permission.yaml b/go/lang/correctness/permissions/file_permission.yaml new file mode 100644 index 00000000..3f3453c4 --- /dev/null +++ b/go/lang/correctness/permissions/file_permission.yaml @@ -0,0 +1,31 @@ +rules: + - id: incorrect-default-permission + message: + Detected file permissions that are set to more than `0600` (user/owner can read and write). Setting file permissions + to higher than `0600` is most likely unnecessary and violates the principle of least privilege. Instead, set permissions + to be `0600` or less for os.Chmod, os.Mkdir, os.OpenFile, os.MkdirAll, and ioutil.WriteFile + metadata: + cwe: "CWE-276: Incorrect Default Permissions" + source_rule_url: https://github.com/securego/gosec + category: correctness + references: + - https://github.com/securego/gosec/blob/master/rules/fileperms.go + technology: + - go + severity: WARNING + languages: [go] + patterns: + - pattern-either: + - pattern: os.Chmod($NAME, $PERM) + - pattern: os.Mkdir($NAME, $PERM) + - pattern: os.OpenFile($NAME, $FLAG, $PERM) + - pattern: os.MkdirAll($NAME, $PERM) + - pattern: ioutil.WriteFile($NAME, $DATA, $PERM) + - metavariable-comparison: + metavariable: $PERM + comparison: $PERM > 0o600 + base: 8 + - focus-metavariable: + - $PERM + fix: | + 0600 diff --git a/go/lang/correctness/use-filepath-join.go b/go/lang/correctness/use-filepath-join.go new file mode 100644 index 00000000..3d344fad --- /dev/null +++ b/go/lang/correctness/use-filepath-join.go @@ -0,0 +1,40 @@ +package main + +import ( + "filepath" + "path" +) + +func a() { + dir := getDir() + + // ok: use-filepath-join + var p = path.Join(getDir()) + // ok: use-filepath-join + var fpath = filepath.Join(getDir()) + + // ruleid: use-filepath-join + path.Join("/", path.Base(p)) +} + +func a() { + url, err := url.Parse("http://foo:666/bar") + if err != nil { + panic(err) + } + + // ok: use-filepath-join + fmt.Println(path.Join(url.Path, "baz")) +} + +func a(p string) { + // ruleid: use-filepath-join + fmt.Println(path.Join(p, "baz")) + + // ok: use-filepath-join + fmt.Println(path.Join("asdf", "baz")) + + // ok: use-filepath-join + fmt.Println(filepath.Join(a.Path, "baz")) +} + diff --git a/go/lang/correctness/use-filepath-join.yaml b/go/lang/correctness/use-filepath-join.yaml new file mode 100644 index 00000000..288ca0bb --- /dev/null +++ b/go/lang/correctness/use-filepath-join.yaml @@ -0,0 +1,50 @@ +rules: + - id: use-filepath-join + languages: + - go + severity: WARNING + message: "`path.Join(...)` always joins using a forward slash. This may cause + issues on Windows or other systems using a different delimiter. Use + `filepath.Join(...)` instead which uses OS-specific path separators." + metadata: + category: correctness + references: + - https://parsiya.net/blog/2019-03-09-path.join-considered-harmful/ + - https://go.dev/src/path/path.go?s=4034:4066#L145 + likelihood: LOW + impact: HIGH + confidence: LOW + subcategory: + - audit + technology: + - go + mode: taint + pattern-sources: + - patterns: + - pattern: | + ($STR : string) + - pattern-not: | + "..." + - patterns: + - pattern-inside: | + import "path" + ... + - pattern: path.$FUNC(...) + - metavariable-regex: + metavariable: $FUNC + regex: ^(Base|Clean|Dir|Split)$ + - patterns: + - pattern-inside: | + import "path/filepath" + ... + - pattern: filepath.$FUNC(...) + - metavariable-regex: + metavariable: $FUNC + regex: ^(Base|Clean|Dir|FromSlash|Glob|Rel|Split|SplitList|ToSlash|VolumeName)$ + pattern-sinks: + - pattern: path.Join(...) + pattern-sanitizers: + - pattern: | + url.Parse(...) + ... + diff --git a/go/lang/correctness/useless-eqeq.go b/go/lang/correctness/useless-eqeq.go new file mode 100644 index 00000000..8959ed27 --- /dev/null +++ b/go/lang/correctness/useless-eqeq.go @@ -0,0 +1,16 @@ +package main +import "fmt" + +func main() { + fmt.Println("hello world") + var y = "hello"; + // ruleid:eqeq-is-bad + fmt.Println(y == y) + // ok:eqeq-is-bad + assert(y == y) + + // ruleid:hardcoded-eq-true-or-false + if (false) { + fmt.Println("never") + } +} diff --git a/go/lang/correctness/useless-eqeq.yaml b/go/lang/correctness/useless-eqeq.yaml new file mode 100644 index 00000000..c39fe022 --- /dev/null +++ b/go/lang/correctness/useless-eqeq.yaml @@ -0,0 +1,31 @@ +rules: + - id: eqeq-is-bad + patterns: + - pattern-not-inside: assert(...) + - pattern-either: + - pattern: $X == $X + - pattern: $X != $X + - pattern-not: 1 == 1 + message: + Detected useless comparison operation `$X == $X` or `$X != $X`. This will always return 'True' or 'False' and therefore + is not necessary. Instead, remove this comparison operation or use another comparison expression that is not deterministic. + languages: [go] + severity: INFO + metadata: + category: correctness + technology: + - go + - id: hardcoded-eq-true-or-false + message: + Detected useless if statement. 'if (True)' and 'if (False)' always result in the same behavior, and therefore is + not necessary in the code. Remove the 'if (False)' expression completely or just the 'if (True)' comparison depending + on which expression is in the code. + languages: [go] + severity: INFO + pattern-either: + - pattern: if (true) { ... } + - pattern: if (false) { ... } + metadata: + category: correctness + technology: + - go diff --git a/go/lang/maintainability/useless-ifelse.go b/go/lang/maintainability/useless-ifelse.go new file mode 100644 index 00000000..1dc85f7e --- /dev/null +++ b/go/lang/maintainability/useless-ifelse.go @@ -0,0 +1,33 @@ +package main + +import "fmt" + +func main() { + fmt.Println("hello world") + var y = 1 + + if y { + fmt.Println("of course") + } + + // ruleid:useless-if-conditional + if y { + fmt.Println("of course") + } else if y { + fmt.Println("of course other thing") + } + + // ruleid:useless-if-body + if y { + fmt.Println("of course") + } else { + fmt.Println("of course") + } + + fmt.Println("of course2") + fmt.Println(1) + fmt.Println(2) + fmt.Println(3) + fmt.Println("of course2") + +} diff --git a/go/lang/maintainability/useless-ifelse.yaml b/go/lang/maintainability/useless-ifelse.yaml new file mode 100644 index 00000000..f5b9b4bb --- /dev/null +++ b/go/lang/maintainability/useless-ifelse.yaml @@ -0,0 +1,33 @@ +rules: + - id: useless-if-conditional + message: + Detected an if block that checks for the same condition on both branches (`$X`). The second condition check is + useless as it is the same as the first, and therefore can be removed from the code, + languages: [go] + severity: WARNING + pattern: | + if ($X) { + ... + } else if ($X) { + ... + } + metadata: + category: maintainability + technology: + - go + - id: useless-if-body + pattern: | + if ($X) { + $S + } else { + $S + } + message: + Detected identical statements in the if body and the else body of an if-statement. This will lead to the same code + being executed no matter what the if-expression evaluates to. Instead, remove the if statement. + languages: [go] + severity: WARNING + metadata: + category: maintainability + technology: + - go diff --git a/go/lang/security/audit/crypto/bad_imports.go b/go/lang/security/audit/crypto/bad_imports.go new file mode 100644 index 00000000..b7bedfe5 --- /dev/null +++ b/go/lang/security/audit/crypto/bad_imports.go @@ -0,0 +1,64 @@ +package main + +import ( + "crypto/cipher" + "crypto/des" + "crypto/md5" + "crypto/rand" + "crypto/rc4" + "crypto/sha1" + "encoding/hex" + "fmt" + "io" + "net/http" + "net/http/cgi" + "os" +) + +func main1() { + // ruleid: insecure-module-used + cgi.Serve(http.FileServer(http.Dir("/usr/share/doc"))) +} + +func main2() { + // ok: insecure-module-used + block, err := des.NewCipher([]byte("sekritz")) + if err != nil { + panic(err) + } + plaintext := []byte("I CAN HAZ SEKRIT MSG PLZ") + ciphertext := make([]byte, des.BlockSize+len(plaintext)) + iv := ciphertext[:des.BlockSize] + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + panic(err) + } + stream := cipher.NewCFBEncrypter(block, iv) + stream.XORKeyStream(ciphertext[des.BlockSize:], plaintext) + fmt.Println("Secret message is: %s", hex.EncodeToString(ciphertext)) +} + +func main3() { + for _, arg := range os.Args { + // ok: insecure-module-used + fmt.Printf("%x - %s\n", md5.Sum([]byte(arg)), arg) + } +} + +func main4() { + // ok: insecure-module-used + cipher, err := rc4.NewCipher([]byte("sekritz")) + if err != nil { + panic(err) + } + plaintext := []byte("I CAN HAZ SEKRIT MSG PLZ") + ciphertext := make([]byte, len(plaintext)) + cipher.XORKeyStream(ciphertext, plaintext) + fmt.Println("Secret message is: %s", hex.EncodeToString(ciphertext)) +} + +func main5() { + for _, arg := range os.Args { + // ok: insecure-module-used + fmt.Printf("%x - %s\n", sha1.Sum([]byte(arg)), arg) + } +} diff --git a/go/lang/security/audit/crypto/bad_imports.yaml b/go/lang/security/audit/crypto/bad_imports.yaml new file mode 100644 index 00000000..59cfdc21 --- /dev/null +++ b/go/lang/security/audit/crypto/bad_imports.yaml @@ -0,0 +1,32 @@ +rules: +- id: insecure-module-used + message: >- + The package `net/http/cgi` is on the import blocklist. + The package is vulnerable to httpoxy attacks (CVE-2015-5386). + It is recommended to use `net/http` or a web framework to build a web application instead. + metadata: + owasp: + - A03:2017 - Sensitive Data Exposure + - A02:2021 - Cryptographic Failures + cwe: + - 'CWE-327: Use of a Broken or Risky Cryptographic Algorithm' + source-rule-url: https://github.com/securego/gosec + references: + - https://godoc.org/golang.org/x/crypto/sha3 + category: security + technology: + - go + confidence: MEDIUM + subcategory: + - audit + likelihood: MEDIUM + impact: MEDIUM + languages: [go] + severity: WARNING + pattern-either: + - patterns: + - pattern-inside: | + import "net/http/cgi" + ... + - pattern: | + cgi.$FUNC(...) diff --git a/go/lang/security/audit/crypto/insecure_ssh.go b/go/lang/security/audit/crypto/insecure_ssh.go new file mode 100644 index 00000000..9074aba6 --- /dev/null +++ b/go/lang/security/audit/crypto/insecure_ssh.go @@ -0,0 +1,23 @@ +package main + +import ( + "golang.org/x/crypto/ssh" +) + +func ok() { + var publicKey *rsa.PublicKey + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, err + } + publicKey = &privateKey.PublicKey + hostKey, _ := ssh.NewPublicKey(publicKey) + // ok: avoid-ssh-insecure-ignore-host-key + _ = ssh.FixedHostKey(hostKey); +} + +func main() { + // ruleid: avoid-ssh-insecure-ignore-host-key + _ = ssh.InsecureIgnoreHostKey() +} diff --git a/go/lang/security/audit/crypto/insecure_ssh.yaml b/go/lang/security/audit/crypto/insecure_ssh.yaml new file mode 100644 index 00000000..206b4747 --- /dev/null +++ b/go/lang/security/audit/crypto/insecure_ssh.yaml @@ -0,0 +1,29 @@ +rules: +- id: avoid-ssh-insecure-ignore-host-key + message: >- + Disabled host key verification detected. This allows man-in-the-middle + attacks. Use the 'golang.org/x/crypto/ssh/knownhosts' package to do + host key verification. + See https://skarlso.github.io/2019/02/17/go-ssh-with-host-key-verification/ + to learn more about the problem and how to fix it. + metadata: + cwe: + - 'CWE-322: Key Exchange without Entity Authentication' + owasp: + - A02:2021 - Cryptographic Failures + source-rule-url: https://github.com/securego/gosec + references: + - https://skarlso.github.io/2019/02/17/go-ssh-with-host-key-verification/ + - https://gist.github.com/Skarlso/34321a230cf0245018288686c9e70b2d + category: security + technology: + - go + confidence: MEDIUM + subcategory: + - audit + likelihood: LOW + impact: LOW + languages: [go] + severity: WARNING + pattern: |- + ssh.InsecureIgnoreHostKey() diff --git a/go/lang/security/audit/crypto/math_random.fixed.go b/go/lang/security/audit/crypto/math_random.fixed.go new file mode 100644 index 00000000..66f18e42 --- /dev/null +++ b/go/lang/security/audit/crypto/math_random.fixed.go @@ -0,0 +1,46 @@ +package main + +import ( + "crypto/rand" + // ruleid: math-random-used + mrand "crypto/rand" + // ruleid: math-random-used + mrand "crypto/rand" + // ruleid: math-random-used + mrand "crypto/rand" + // ok: math-random-used + mrand "math/rand/something" +) + +func main() { + main0() + main1() + main2() + main3() +} + +func main0() { + // ok: math-random-used + bad, _ := mrand.Read(nil) + println(bad) +} + +func main1() { + // ok: math-random-used + good, _ := rand.Read(nil) + println(good) +} + +func main2() { + // ok: math-random-used + bad := mrand.Int() + println(bad) +} + +func main3() { + // ok: math-random-used + good, _ := rand.Read(nil) + println(good) + i := mrand.Int31() + println(i) +} diff --git a/go/lang/security/audit/crypto/math_random.go b/go/lang/security/audit/crypto/math_random.go new file mode 100644 index 00000000..7192833e --- /dev/null +++ b/go/lang/security/audit/crypto/math_random.go @@ -0,0 +1,46 @@ +package main + +import ( + "crypto/rand" + // ruleid: math-random-used + mrand "math/rand" + // ruleid: math-random-used + mrand "math/rand/v2" + // ruleid: math-random-used + mrand "math/rand/v222" + // ok: math-random-used + mrand "math/rand/something" +) + +func main() { + main0() + main1() + main2() + main3() +} + +func main0() { + // ok: math-random-used + bad, _ := mrand.Read(nil) + println(bad) +} + +func main1() { + // ok: math-random-used + good, _ := rand.Read(nil) + println(good) +} + +func main2() { + // ok: math-random-used + bad := mrand.Int() + println(bad) +} + +func main3() { + // ok: math-random-used + good, _ := rand.Read(nil) + println(good) + i := mrand.Int31() + println(i) +} diff --git a/go/lang/security/audit/crypto/math_random.yaml b/go/lang/security/audit/crypto/math_random.yaml new file mode 100644 index 00000000..feef3816 --- /dev/null +++ b/go/lang/security/audit/crypto/math_random.yaml @@ -0,0 +1,40 @@ +rules: +- id: math-random-used + metadata: + cwe: + - 'CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG)' + owasp: + - A02:2021 - Cryptographic Failures + references: + - https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html#secure-random-number-generation + category: security + technology: + - go + confidence: MEDIUM + subcategory: + - vuln + likelihood: MEDIUM + impact: MEDIUM + message: Do not use `math/rand`. Use `crypto/rand` instead. + languages: [go] + severity: WARNING + patterns: + - pattern-either: + - pattern: | + import $RAND "$MATH" + - pattern: | + import "$MATH" + - metavariable-regex: + metavariable: $MATH + regex: ^(math/rand(\/v[0-9]+)*)$ + - pattern-either: + - pattern-inside: | + ... + rand.$FUNC(...) + - pattern-inside: | + ... + $RAND.$FUNC(...) + - focus-metavariable: + - $MATH + fix: | + crypto/rand diff --git a/go/lang/security/audit/crypto/missing-ssl-minversion.fixed.go b/go/lang/security/audit/crypto/missing-ssl-minversion.fixed.go new file mode 100644 index 00000000..681da872 --- /dev/null +++ b/go/lang/security/audit/crypto/missing-ssl-minversion.fixed.go @@ -0,0 +1,68 @@ +package main + +import ( + "crypto/tls" + "log" + "net/http" + "net/http/httptest" + "os" +) + +// zeroSource is an io.Reader that returns an unlimited number of zero bytes. +type zeroSource struct{} + +func (zeroSource) Read(b []byte) (n int, err error) { + for i := range b { + b[i] = 0 + } + + return len(b), nil +} + +func main() { + // Dummy test HTTP server for the example with insecure random so output is + // reproducible. + server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + // ruleid: missing-ssl-minversion + server.TLS = &tls.Config{ Rand: zeroSource{}, MinVersion: tls.VersionTLS13 } + server.StartTLS() + defer server.Close() + + // Typically the log would go to an open file: + // w, err := os.OpenFile("tls-secrets.txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + w := os.Stdout + + client := &http.Client{ + Transport: &http.Transport{ + // ok: missing-ssl-minversion + TLSClientConfig: &tls.Config{ + KeyLogWriter: w, + MinVersion: tls.VersionSSL30, + Rand: zeroSource{}, // for reproducible output; don't do this. + InsecureSkipVerify: true, // test server certificate is not trusted. + }, + }, + } + resp, err := client.Get(server.URL) + if err != nil { + log.Fatalf("Failed to get URL: %v", err) + } + resp.Body.Close() + + clientGood := &http.Client{ + Transport: &http.Transport{ + // ok: missing-ssl-minversion + TLSClientConfig: &tls.Config{ + KeyLogWriter: w, + MinVersion: tls.VersionTLS10, + Rand: zeroSource{}, // for reproducible output; don't do this. + InsecureSkipVerify: true, // test server certificate is not trusted. + }, + }, + } + resp, err := client.Get(server.URL) + if err != nil { + log.Fatalf("Failed to get URL: %v", err) + } + resp.Body.Close() +} diff --git a/go/lang/security/audit/crypto/missing-ssl-minversion.go b/go/lang/security/audit/crypto/missing-ssl-minversion.go new file mode 100644 index 00000000..cd4ab1c6 --- /dev/null +++ b/go/lang/security/audit/crypto/missing-ssl-minversion.go @@ -0,0 +1,70 @@ +package main + +import ( + "crypto/tls" + "log" + "net/http" + "net/http/httptest" + "os" +) + +// zeroSource is an io.Reader that returns an unlimited number of zero bytes. +type zeroSource struct{} + +func (zeroSource) Read(b []byte) (n int, err error) { + for i := range b { + b[i] = 0 + } + + return len(b), nil +} + +func main() { + // Dummy test HTTP server for the example with insecure random so output is + // reproducible. + server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + // ruleid: missing-ssl-minversion + server.TLS = &tls.Config{ + Rand: zeroSource{}, // for example only; don't do this. + } + server.StartTLS() + defer server.Close() + + // Typically the log would go to an open file: + // w, err := os.OpenFile("tls-secrets.txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + w := os.Stdout + + client := &http.Client{ + Transport: &http.Transport{ + // ok: missing-ssl-minversion + TLSClientConfig: &tls.Config{ + KeyLogWriter: w, + MinVersion: tls.VersionSSL30, + Rand: zeroSource{}, // for reproducible output; don't do this. + InsecureSkipVerify: true, // test server certificate is not trusted. + }, + }, + } + resp, err := client.Get(server.URL) + if err != nil { + log.Fatalf("Failed to get URL: %v", err) + } + resp.Body.Close() + + clientGood := &http.Client{ + Transport: &http.Transport{ + // ok: missing-ssl-minversion + TLSClientConfig: &tls.Config{ + KeyLogWriter: w, + MinVersion: tls.VersionTLS10, + Rand: zeroSource{}, // for reproducible output; don't do this. + InsecureSkipVerify: true, // test server certificate is not trusted. + }, + }, + } + resp, err := client.Get(server.URL) + if err != nil { + log.Fatalf("Failed to get URL: %v", err) + } + resp.Body.Close() +} diff --git a/go/lang/security/audit/crypto/missing-ssl-minversion.yaml b/go/lang/security/audit/crypto/missing-ssl-minversion.yaml new file mode 100644 index 00000000..fdbcf08f --- /dev/null +++ b/go/lang/security/audit/crypto/missing-ssl-minversion.yaml @@ -0,0 +1,38 @@ +rules: +- id: missing-ssl-minversion + message: >- + `MinVersion` is missing from this TLS configuration. + By default, TLS 1.2 is currently used as the minimum when acting as a client, and TLS 1.0 when acting as a server. + General purpose web applications should default to TLS 1.3 with all other protocols disabled. + Only where it is known that a web server must support legacy clients + with unsupported an insecure browsers (such as Internet Explorer 10), it may be necessary to enable TLS 1.0 to provide support. + Add `MinVersion: tls.VersionTLS13' to the TLS configuration to bump the minimum version to TLS 1.3. + metadata: + cwe: + - 'CWE-327: Use of a Broken or Risky Cryptographic Algorithm' + owasp: + - A03:2017 - Sensitive Data Exposure + - A02:2021 - Cryptographic Failures + source-rule-url: https://github.com/securego/gosec/blob/master/rules/tls_config.go + references: + - https://golang.org/doc/go1.14#crypto/tls + - https://golang.org/pkg/crypto/tls/#:~:text=MinVersion + - https://www.us-cert.gov/ncas/alerts/TA14-290A + category: security + technology: + - go + confidence: HIGH + subcategory: + - audit + likelihood: MEDIUM + impact: LOW + languages: [go] + severity: WARNING + patterns: + - pattern: | + tls.Config{ $...CONF } + - pattern-not: | + tls.Config{..., MinVersion: ..., ...} + fix: | + tls.Config{ $...CONF, MinVersion: tls.VersionTLS13 } + diff --git a/go/lang/security/audit/crypto/sha224-hash.go b/go/lang/security/audit/crypto/sha224-hash.go new file mode 100644 index 00000000..09d8d97b --- /dev/null +++ b/go/lang/security/audit/crypto/sha224-hash.go @@ -0,0 +1,43 @@ +package main + +import ( + "crypto/sha256" + "golang.org/x/crypto/sha3" + "fmt" + "io" + "log" + "os" +) + +func main() { +} + +func test_sha224() { + f, err := os.Open("file.txt") + if err != nil { + log.Fatal(err) + } + defer f.Close() + // ruleid: sha224-hash + h := sha256.New224() + if _, err := io.Copy(h, f); err != nil { + log.Fatal(err) + } + // ruleid: sha224-hash + fmt.Printf("%x", sha256.Sum224(nil)) +} + +func test_sha3_224() { + f, err := os.Open("file.txt") + if err != nil { + log.Fatal(err) + } + defer f.Close() + // ruleid: sha224-hash + h := sha3.New224() + if _, err := io.Copy(h, f); err != nil { + log.Fatal(err) + } + // ruleid: sha224-hash + fmt.Printf("%x", sha3.Sum224(nil)) +} \ No newline at end of file diff --git a/go/lang/security/audit/crypto/sha224-hash.yaml b/go/lang/security/audit/crypto/sha224-hash.yaml new file mode 100644 index 00000000..8fe39e05 --- /dev/null +++ b/go/lang/security/audit/crypto/sha224-hash.yaml @@ -0,0 +1,44 @@ +rules: +- id: sha224-hash + pattern-either: + - patterns: + - pattern-inside: | + import "crypto/sha256" + ... + - pattern-either: + - pattern: | + sha256.New224() + - pattern: | + sha256.Sum224(...) + - patterns: + - pattern-inside: | + import "golang.org/x/crypto/sha3" + ... + - pattern-either: + - pattern: | + sha3.New224() + - pattern: | + sha3.Sum224(...) + message: >- + This code uses a 224-bit hash function, which is deprecated or disallowed + in some security policies. Consider updating to a stronger hash function such + as SHA-384 or higher to ensure compliance and security. + languages: [go] + severity: WARNING + metadata: + owasp: + - A03:2017 - Sensitive Data Exposure + - A02:2021 - Cryptographic Failures + cwe: + - 'CWE-328: Use of Weak Hash' + category: security + technology: + - go + references: + - https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-131Ar3.ipd.pdf + - https://www.cyber.gov.au/resources-business-and-government/essential-cyber-security/ism/cyber-security-guidelines/guidelines-cryptography + subcategory: + - vuln + likelihood: LOW + impact: LOW + confidence: HIGH \ No newline at end of file diff --git a/go/lang/security/audit/crypto/ssl.go b/go/lang/security/audit/crypto/ssl.go new file mode 100644 index 00000000..d69d77ec --- /dev/null +++ b/go/lang/security/audit/crypto/ssl.go @@ -0,0 +1,69 @@ +package main + +import ( + "crypto/tls" + "log" + "net/http" + "net/http/httptest" + "os" +) + +// zeroSource is an io.Reader that returns an unlimited number of zero bytes. +type zeroSource struct{} + +func (zeroSource) Read(b []byte) (n int, err error) { + for i := range b { + b[i] = 0 + } + + return len(b), nil +} + +func main() { + // Dummy test HTTP server for the example with insecure random so output is + // reproducible. + server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + server.TLS = &tls.Config{ + Rand: zeroSource{}, // for example only; don't do this. + } + server.StartTLS() + defer server.Close() + + // Typically the log would go to an open file: + // w, err := os.OpenFile("tls-secrets.txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + w := os.Stdout + + client := &http.Client{ + Transport: &http.Transport{ + // ruleid: ssl-v3-is-insecure + TLSClientConfig: &tls.Config{ + KeyLogWriter: w, + MinVersion: tls.VersionSSL30, + Rand: zeroSource{}, // for reproducible output; don't do this. + InsecureSkipVerify: true, // test server certificate is not trusted. + }, + }, + } + resp, err := client.Get(server.URL) + if err != nil { + log.Fatalf("Failed to get URL: %v", err) + } + resp.Body.Close() + + client_good := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + KeyLogWriter: w, + // OK + MinVersion: tls.VersionTLS10, + Rand: zeroSource{}, // for reproducible output; don't do this. + InsecureSkipVerify: true, // test server certificate is not trusted. + }, + }, + } + resp, err := client.Get(server.URL) + if err != nil { + log.Fatalf("Failed to get URL: %v", err) + } + resp.Body.Close() +} diff --git a/go/lang/security/audit/crypto/ssl.yaml b/go/lang/security/audit/crypto/ssl.yaml new file mode 100644 index 00000000..330a34b9 --- /dev/null +++ b/go/lang/security/audit/crypto/ssl.yaml @@ -0,0 +1,30 @@ +rules: +- id: ssl-v3-is-insecure + message: >- + SSLv3 is insecure because it has known vulnerabilities. + Starting with go1.14, SSLv3 will be removed. Instead, use + 'tls.VersionTLS13'. + metadata: + cwe: + - 'CWE-327: Use of a Broken or Risky Cryptographic Algorithm' + owasp: + - A03:2017 - Sensitive Data Exposure + - A02:2021 - Cryptographic Failures + source-rule-url: https://github.com/securego/gosec/blob/master/rules/tls_config.go + references: + - https://golang.org/doc/go1.14#crypto/tls + - https://www.us-cert.gov/ncas/alerts/TA14-290A + category: security + technology: + - go + confidence: HIGH + subcategory: + - vuln + likelihood: MEDIUM + impact: LOW + languages: [go] + severity: WARNING + fix-regex: + regex: VersionSSL30 + replacement: VersionTLS13 + pattern: 'tls.Config{..., MinVersion: $TLS.VersionSSL30, ...}' diff --git a/go/lang/security/audit/crypto/tls.go b/go/lang/security/audit/crypto/tls.go new file mode 100644 index 00000000..af84aa4b --- /dev/null +++ b/go/lang/security/audit/crypto/tls.go @@ -0,0 +1,32 @@ +// Insecure ciphersuite selection +package main + +import ( + "crypto/tls" + "fmt" + "net/http" +) + +func main() { + tr := &http.Transport{ + // ruleid: tls-with-insecure-cipher + TLSClientConfig: &tls.Config{CipherSuites: []uint16{ + tls.TLS_RSA_WITH_RC4_128_SHA, + tls.TLS_RSA_WITH_AES_128_CBC_SHA256, + }}, + } + client := &http.Client{Transport: tr} + _, err := client.Get("https://golang.org/") + if err != nil { + fmt.Println(err) + } + + tr := &http.Transport{ + // should be fine + TLSClientConfig: &tls.Config{CipherSuites: []uint16{ + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_AES_256_GCM_SHA384, + }}, + } + client := &http.Client{Transport: tr} +} diff --git a/go/lang/security/audit/crypto/tls.yaml b/go/lang/security/audit/crypto/tls.yaml new file mode 100644 index 00000000..c02dad3f --- /dev/null +++ b/go/lang/security/audit/crypto/tls.yaml @@ -0,0 +1,60 @@ +rules: +- id: tls-with-insecure-cipher + message: >- + Detected an insecure CipherSuite via the 'tls' module. This suite is considered + weak. + Use the function 'tls.CipherSuites()' to get a list of good cipher suites. + See https://golang.org/pkg/crypto/tls/#InsecureCipherSuites + for why and what other cipher suites to use. + metadata: + cwe: + - 'CWE-327: Use of a Broken or Risky Cryptographic Algorithm' + owasp: + - A03:2017 - Sensitive Data Exposure + - A02:2021 - Cryptographic Failures + source-rule-url: https://github.com/securego/gosec/blob/master/rules/tls.go + references: + - https://golang.org/pkg/crypto/tls/#InsecureCipherSuites + category: security + technology: + - go + confidence: HIGH + subcategory: + - vuln + likelihood: HIGH + impact: LOW + languages: [go] + severity: WARNING + pattern-either: + - pattern: | + tls.Config{..., CipherSuites: []$TYPE{..., tls.TLS_RSA_WITH_RC4_128_SHA, ...}} + - pattern: | + tls.Config{..., CipherSuites: []$TYPE{..., tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, ...}} + - pattern: | + tls.Config{..., CipherSuites: []$TYPE{..., tls.TLS_RSA_WITH_AES_128_CBC_SHA256, ...}} + - pattern: | + tls.Config{..., CipherSuites: []$TYPE{..., tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, ...}} + - pattern: | + tls.Config{..., CipherSuites: []$TYPE{..., tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, ...}} + - pattern: | + tls.Config{..., CipherSuites: []$TYPE{..., tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, ...}} + - pattern: | + tls.Config{..., CipherSuites: []$TYPE{..., tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, ...}} + - pattern: | + tls.Config{..., CipherSuites: []$TYPE{..., tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, ...}} + - pattern: | + tls.CipherSuite{..., TLS_RSA_WITH_RC4_128_SHA, ...} + - pattern: | + tls.CipherSuite{..., TLS_RSA_WITH_3DES_EDE_CBC_SHA, ...} + - pattern: | + tls.CipherSuite{..., TLS_RSA_WITH_AES_128_CBC_SHA256, ...} + - pattern: | + tls.CipherSuite{..., TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, ...} + - pattern: | + tls.CipherSuite{..., TLS_ECDHE_RSA_WITH_RC4_128_SHA, ...} + - pattern: | + tls.CipherSuite{..., TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, ...} + - pattern: | + tls.CipherSuite{..., TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, ...} + - pattern: | + tls.CipherSuite{..., TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, ...} diff --git a/go/lang/security/audit/crypto/use_of_weak_crypto.go b/go/lang/security/audit/crypto/use_of_weak_crypto.go new file mode 100644 index 00000000..ca544bce --- /dev/null +++ b/go/lang/security/audit/crypto/use_of_weak_crypto.go @@ -0,0 +1,79 @@ +package main + +import ( + "crypto/des" + "crypto/md5" + "crypto/rc4" + "crypto/sha1" + "fmt" + "io" + "log" + "os" +) + +func main() { +} + +func test_des() { + // NewTripleDESCipher can also be used when EDE2 is required by + // duplicating the first 8 bytes of the 16-byte key. + ede2Key := []byte("example key 1234") + + var tripleDESKey []byte + tripleDESKey = append(tripleDESKey, ede2Key[:16]...) + tripleDESKey = append(tripleDESKey, ede2Key[:8]...) + // ruleid: use-of-DES + _, err := des.NewTripleDESCipher(tripleDESKey) + if err != nil { + panic(err) + } + + // See crypto/cipher for how to use a cipher.Block for encryption and + // decryption. +} + +func test_md5() { + f, err := os.Open("file.txt") + if err != nil { + log.Fatal(err) + } + defer f.Close() + + defer func() { + err := f.Close() + if err != nil { + log.Printf("error closing the file: %s", err) + } + }() + + // ruleid: use-of-md5 + h := md5.New() + if _, err := io.Copy(h, f); err != nil { + log.Fatal(err) + } + // ruleid: use-of-md5 + fmt.Printf("%x", md5.Sum(nil)) +} + +func test_rc4() { + key := []byte{1, 2, 3, 4, 5, 6, 7} + // ruleid: use-of-rc4 + c, err := rc4.NewCipher(key) + dst := make([]byte, len(src)) + c.XORKeyStream(dst, src) +} + +func test_sha1() { + f, err := os.Open("file.txt") + if err != nil { + log.Fatal(err) + } + defer f.Close() + // ruleid: use-of-sha1 + h := sha1.New() + if _, err := io.Copy(h, f); err != nil { + log.Fatal(err) + } + // ruleid: use-of-sha1 + fmt.Printf("%x", sha1.Sum(nil)) +} \ No newline at end of file diff --git a/go/lang/security/audit/crypto/use_of_weak_crypto.yaml b/go/lang/security/audit/crypto/use_of_weak_crypto.yaml new file mode 100644 index 00000000..3c8e6175 --- /dev/null +++ b/go/lang/security/audit/crypto/use_of_weak_crypto.yaml @@ -0,0 +1,128 @@ +rules: +- id: use-of-md5 + message: >- + Detected MD5 hash algorithm which is considered insecure. MD5 is not + collision resistant and is therefore not suitable as a cryptographic + signature. Use SHA256 or SHA3 instead. + languages: [go] + severity: WARNING + metadata: + owasp: + - A03:2017 - Sensitive Data Exposure + - A02:2021 - Cryptographic Failures + cwe: + - 'CWE-328: Use of Weak Hash' + source-rule-url: https://github.com/securego/gosec#available-rules + category: security + technology: + - go + confidence: MEDIUM + references: + - https://owasp.org/Top10/A02_2021-Cryptographic_Failures + subcategory: + - vuln + likelihood: MEDIUM + impact: MEDIUM + patterns: + - pattern-inside: | + import "crypto/md5" + ... + - pattern-either: + - pattern: | + md5.New() + - pattern: | + md5.Sum(...) +- id: use-of-sha1 + message: >- + Detected SHA1 hash algorithm which is considered insecure. SHA1 is not + collision resistant and is therefore not suitable as a cryptographic + signature. Use SHA256 or SHA3 instead. + languages: [go] + severity: WARNING + metadata: + owasp: + - A03:2017 - Sensitive Data Exposure + - A02:2021 - Cryptographic Failures + cwe: + - 'CWE-328: Use of Weak Hash' + source-rule-url: https://github.com/securego/gosec#available-rules + category: security + technology: + - go + references: + - https://owasp.org/Top10/A02_2021-Cryptographic_Failures + subcategory: + - vuln + likelihood: MEDIUM + impact: MEDIUM + confidence: MEDIUM + patterns: + - pattern-inside: | + import "crypto/sha1" + ... + - pattern-either: + - pattern: | + sha1.New() + - pattern: | + sha1.Sum(...) +- id: use-of-DES + message: >- + Detected DES cipher algorithm which is insecure. The algorithm is + considered weak and has been deprecated. Use AES instead. + languages: [go] + severity: WARNING + metadata: + owasp: + - A03:2017 - Sensitive Data Exposure + - A02:2021 - Cryptographic Failures + cwe: + - 'CWE-327: Use of a Broken or Risky Cryptographic Algorithm' + source-rule-url: https://github.com/securego/gosec#available-rules + category: security + technology: + - go + references: + - https://owasp.org/Top10/A02_2021-Cryptographic_Failures + subcategory: + - vuln + likelihood: MEDIUM + impact: MEDIUM + confidence: MEDIUM + patterns: + - pattern-inside: | + import "crypto/des" + ... + - pattern-either: + - pattern: | + des.NewTripleDESCipher(...) + - pattern: | + des.NewCipher(...) +- id: use-of-rc4 + message: >- + Detected RC4 cipher algorithm which is insecure. The algorithm has many + known vulnerabilities. Use AES instead. + languages: [go] + severity: WARNING + metadata: + owasp: + - A03:2017 - Sensitive Data Exposure + - A02:2021 - Cryptographic Failures + cwe: + - 'CWE-327: Use of a Broken or Risky Cryptographic Algorithm' + source-rule-url: https://github.com/securego/gosec#available-rules + category: security + technology: + - go + references: + - https://owasp.org/Top10/A02_2021-Cryptographic_Failures + subcategory: + - vuln + likelihood: MEDIUM + impact: MEDIUM + confidence: MEDIUM + patterns: + - pattern-inside: | + import "crypto/rc4" + ... + - pattern: |- + rc4.NewCipher(...) diff --git a/go/lang/security/audit/crypto/use_of_weak_rsa_key.fixed.go b/go/lang/security/audit/crypto/use_of_weak_rsa_key.fixed.go new file mode 100644 index 00000000..5e0846cc --- /dev/null +++ b/go/lang/security/audit/crypto/use_of_weak_rsa_key.fixed.go @@ -0,0 +1,24 @@ +package main + +import ( + "crypto/rand" + "crypto/rsa" + "fmt" +) + +func main() { + //Generate Private Key + // ruleid: use-of-weak-rsa-key + pvk, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + fmt.Println(err) + } + fmt.Println(pvk) + + // ok: use-of-weak-rsa-key + pvk, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + fmt.Println(err) + } + fmt.Println(pvk) +} diff --git a/go/lang/security/audit/crypto/use_of_weak_rsa_key.go b/go/lang/security/audit/crypto/use_of_weak_rsa_key.go new file mode 100644 index 00000000..0c792586 --- /dev/null +++ b/go/lang/security/audit/crypto/use_of_weak_rsa_key.go @@ -0,0 +1,24 @@ +package main + +import ( + "crypto/rand" + "crypto/rsa" + "fmt" +) + +func main() { + //Generate Private Key + // ruleid: use-of-weak-rsa-key + pvk, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + fmt.Println(err) + } + fmt.Println(pvk) + + // ok: use-of-weak-rsa-key + pvk, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + fmt.Println(err) + } + fmt.Println(pvk) +} diff --git a/go/lang/security/audit/crypto/use_of_weak_rsa_key.yaml b/go/lang/security/audit/crypto/use_of_weak_rsa_key.yaml new file mode 100644 index 00000000..8a35aa7b --- /dev/null +++ b/go/lang/security/audit/crypto/use_of_weak_rsa_key.yaml @@ -0,0 +1,35 @@ +rules: +- id: use-of-weak-rsa-key + message: RSA keys should be at least 2048 bits + languages: [go] + severity: WARNING + metadata: + cwe: + - 'CWE-326: Inadequate Encryption Strength' + owasp: + - A03:2017 - Sensitive Data Exposure + - A02:2021 - Cryptographic Failures + source-rule-url: https://github.com/securego/gosec/blob/master/rules/rsa.go + references: + - https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html#algorithms + category: security + technology: + - go + confidence: HIGH + subcategory: + - audit + likelihood: HIGH + impact: MEDIUM + patterns: + - pattern-either: + - pattern: | + rsa.GenerateKey(..., $BITS) + - pattern: | + rsa.GenerateMultiPrimeKey(..., $BITS) + - metavariable-comparison: + metavariable: $BITS + comparison: $BITS < 2048 + - focus-metavariable: + - $BITS + fix: | + 2048 diff --git a/go/lang/security/audit/dangerous-command-write.go b/go/lang/security/audit/dangerous-command-write.go new file mode 100644 index 00000000..5bb14c01 --- /dev/null +++ b/go/lang/security/audit/dangerous-command-write.go @@ -0,0 +1,30 @@ + import ( + "fmt" + "os" + "os/exec" +) + +func test1(password string) { + cmd := exec.Command("bash") + cmdWriter, _ := cmd.StdinPipe() + cmd.Start() + + cmdString := fmt.Sprintf("sshpass -p %s", password) + + // ruleid:dangerous-command-write + cmdWriter.Write([]byte(cmdString + "\n")) + + cmd.Wait() +} + +func okTest1() { + cmd := exec.Command("bash") + cmdWriter, _ := cmd.StdinPipe() + cmd.Start() + + // ok:dangerous-command-write + cmdWriter.Write([]byte("sshpass -p 123\n")) + cmdWriter.Write([]byte("exit" + "\n")) + + cmd.Wait() +} diff --git a/go/lang/security/audit/dangerous-command-write.yaml b/go/lang/security/audit/dangerous-command-write.yaml new file mode 100644 index 00000000..fc1b72a5 --- /dev/null +++ b/go/lang/security/audit/dangerous-command-write.yaml @@ -0,0 +1,48 @@ +rules: +- id: dangerous-command-write + patterns: + - pattern: | + $CW.Write($BYTE) + - pattern-inside: | + $CW,$ERR := $CMD.StdinPipe() + ... + - pattern-not: | + $CW.Write("...") + - pattern-not: | + $CW.Write([]byte("...")) + - pattern-not: | + $CW.Write([]byte("..."+"...")) + - pattern-not-inside: | + $BYTE = []byte("..."); + ... + - pattern-not-inside: | + $BYTE = []byte("..."+"..."); + ... + - pattern-inside: | + import "os/exec" + ... + message: >- + Detected non-static command inside Write. Audit the input to '$CW.Write'. + If unverified user data can reach this call site, this is a code injection + vulnerability. A malicious actor can inject a malicious script to execute + arbitrary code. + severity: ERROR + languages: [go] + metadata: + cwe: + - "CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')" + category: security + technology: + - go + confidence: LOW + owasp: + - A01:2017 - Injection + - A03:2021 - Injection + references: + - https://owasp.org/Top10/A03_2021-Injection + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - audit + likelihood: LOW + impact: HIGH diff --git a/go/lang/security/audit/dangerous-exec-cmd.go b/go/lang/security/audit/dangerous-exec-cmd.go new file mode 100644 index 00000000..41a23e53 --- /dev/null +++ b/go/lang/security/audit/dangerous-exec-cmd.go @@ -0,0 +1,89 @@ +package main + +import ( + "fmt" + "os" + "os/exec" +) + +func test1(userInput string) { + + cmdPath,_ := userInput; + + // ruleid:dangerous-exec-cmd + cmd := &exec.Cmd { + Path: cmdPath, + Args: []string{ "foo", "bar" }, + Stdout: os.Stdout, + Stderr: os.Stdout, + } + + cmd.Start(); + +} + +func test2(userInput string) { + + cmdPath,_ := exec.LookPath("foo"); + + // ruleid:dangerous-exec-cmd + cmd := &exec.Cmd { + Path: cmdPath, + Args: []string{ userInput, "bar" }, + Stdout: os.Stdout, + Stderr: os.Stdout, + } + + cmd.Start(); + +} + +func test3(userInput string) { + + cmdPath,_ := exec.LookPath("bash"); + + // ruleid:dangerous-exec-cmd + cmd := &exec.Cmd { + Path: cmdPath, + Args: []string{ cmdPath, "-c", userInput }, + Stdout: os.Stdout, + Stderr: os.Stdout, + } + + cmd.Start(); + +} + +func test4(userInput string) { + + cmdPath,_ := exec.LookPath("bash"); + + args = []string{ cmdPath, "-c", userInput } + + // ruleid:dangerous-exec-cmd + cmd := &exec.Cmd { + Path: cmdPath, + Args: args, + Stdout: os.Stdout, + Stderr: os.Stdout, + } + + cmd.Start(); + +} + +func okTest1(userInput string) { + + cmdPath,_ := exec.LookPath("go"); + + // ok:dangerous-exec-cmd + cmd := &exec.Cmd { + Path: cmdPath, + Args: []string{ cmdPath, "bar" }, + Stdout: os.Stdout, + Stderr: os.Stdout, + } + + cmd.Start(); + +} diff --git a/go/lang/security/audit/dangerous-exec-cmd.yaml b/go/lang/security/audit/dangerous-exec-cmd.yaml new file mode 100644 index 00000000..18309121 --- /dev/null +++ b/go/lang/security/audit/dangerous-exec-cmd.yaml @@ -0,0 +1,85 @@ +rules: +- id: dangerous-exec-cmd + patterns: + - pattern-either: + - patterns: + - pattern: | + exec.Cmd {...,Path: $CMD,...} + - pattern-not: | + exec.Cmd {...,Path: "...",...} + - pattern-not-inside: | + $CMD,$ERR := exec.LookPath("..."); + ... + - pattern-not-inside: | + $CMD = "..."; + ... + - patterns: + - pattern: | + exec.Cmd {...,Args: $ARGS,...} + - pattern-not: | + exec.Cmd {...,Args: []string{...},...} + - pattern-not-inside: | + $ARGS = []string{"...",...}; + ... + - pattern-not-inside: | + $CMD = "..."; + ... + $ARGS = []string{$CMD,...}; + ... + - pattern-not-inside: | + $CMD = exec.LookPath("..."); + ... + $ARGS = []string{$CMD,...}; + ... + - patterns: + - pattern: | + exec.Cmd {...,Args: []string{$CMD,...},...} + - pattern-not: | + exec.Cmd {...,Args: []string{"...",...},...} + - pattern-not-inside: | + $CMD,$ERR := exec.LookPath("..."); + ... + - pattern-not-inside: | + $CMD = "..."; + ... + - patterns: + - pattern-either: + - pattern: | + exec.Cmd {...,Args: []string{"=~/(sh|bash|ksh|csh|tcsh|zsh)/","-c",$EXE,...},...} + - patterns: + - pattern: | + exec.Cmd {...,Args: []string{$CMD,"-c",$EXE,...},...} + - pattern-inside: | + $CMD,$ERR := exec.LookPath("=~/(sh|bash|ksh|csh|tcsh|zsh)/"); + ... + - pattern-not: | + exec.Cmd {...,Args: []string{"...","...","...",...},...} + - pattern-not-inside: | + $EXE = "..."; + ... + - pattern-inside: | + import "os/exec" + ... + message: >- + Detected non-static command inside exec.Cmd. Audit the input to 'exec.Cmd'. + If unverified user data can reach this call site, this is a code injection + vulnerability. A malicious actor can inject a malicious script to execute + arbitrary code. + metadata: + cwe: + - "CWE-94: Improper Control of Generation of Code ('Code Injection')" + owasp: + - A03:2021 - Injection + category: security + technology: + - go + confidence: MEDIUM + references: + - https://owasp.org/Top10/A03_2021-Injection + cwe2022-top25: true + subcategory: + - audit + likelihood: LOW + impact: HIGH + severity: ERROR + languages: [go] diff --git a/go/lang/security/audit/dangerous-exec-command.go b/go/lang/security/audit/dangerous-exec-command.go new file mode 100644 index 00000000..0109f8c2 --- /dev/null +++ b/go/lang/security/audit/dangerous-exec-command.go @@ -0,0 +1,133 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/exec" + "time" +) + +func runCommand1(userInput string) { + // ruleid:dangerous-exec-command + cmd := exec.Command(userInput, "foobar") + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stdout + + if err := cmd.Run(); err != nil { + fmt.Println("Error:", err) + } + +} + +func runCommand2(userInput string) { + + execPath, _ := exec.LookPath(userInput) + + // ruleid:dangerous-exec-command + cmd := exec.Command(execPath, "foobar") + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stdout + + if err := cmd.Run(); err != nil { + fmt.Println("Error:", err) + } + +} + +func runCommand3(userInput string) { + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + + // ruleid:dangerous-exec-command + if err := exec.CommandContext(ctx, userInput, "5").Run(); err != nil { + fmt.Println("Error:", err) + } + +} + +func runCommand4(userInput string) { + + // ruleid:dangerous-exec-command + cmd := exec.Command("bash", "-c", userInput) + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stdout + + if err := cmd.Run(); err != nil { + fmt.Println("Error:", err) + } + +} + +func runcommand5(s string) (string, error) { + + // ruleid:dangerous-exec-command + cmd := exec.Command("/usr/bin/env", "bash", "-c", s) + stdoutStderr, err := cmd.CombinedOutput() + + if err != nil { + return "", fmt.Errorf("shellCommand: unexpected error: out = %s, error = %v", stdoutStderr, err) + } + + return string(stdoutStderr), nil +} + +func runcommand6(s string) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + // might not have user context + // ruleid:dangerous-exec-command + cmd := exec.CommandContext(ctx, "/bin/env", "bash", "-c", s) + stdoutStderr, err := cmd.CombinedOutput() + + if err != nil { + return "", fmt.Errorf("shellCommand: unexpected error: out = %s, error = %v", stdoutStderr, err) + } + + return string(stdoutStderr), nil +} + +func okCommand1(userInput string) { + + goExec, _ := exec.LookPath("go") + + // ok:dangerous-exec-command + cmd := exec.Command(goExec, "version") + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stdout + + if err := cmd.Run(); err != nil { + fmt.Println("Error:", err) + } + +} + +func okCommand2(userInput string) { + // ok:dangerous-exec-command + cmd := exec.Command("go", "version") + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stdout + + if err := cmd.Run(); err != nil { + fmt.Println("Error:", err) + } + +} + +func okCommand3(s string) (string, error) { + + someCommand := "w" + // ok:dangerous-exec-command + cmd := exec.Command("/usr/bin/env", "bash", "-c", someCommand) + stdoutStderr, err := cmd.CombinedOutput() + + if err != nil { + return "", fmt.Errorf("shellCommand: unexpected error: out = %s, error = %v", stdoutStderr, err) + } + + return string(stdoutStderr), nil +} diff --git a/go/lang/security/audit/dangerous-exec-command.yaml b/go/lang/security/audit/dangerous-exec-command.yaml new file mode 100644 index 00000000..30630293 --- /dev/null +++ b/go/lang/security/audit/dangerous-exec-command.yaml @@ -0,0 +1,61 @@ +rules: +- id: dangerous-exec-command + patterns: + - pattern-either: + - patterns: + - pattern-either: + - pattern: | + exec.Command($CMD,...) + - pattern: | + exec.CommandContext($CTX,$CMD,...) + - pattern-not: | + exec.Command("...",...) + - pattern-not: | + exec.CommandContext($CTX,"...",...) + - patterns: + - pattern-either: + - pattern: | + exec.Command("=~/(sh|bash|ksh|csh|tcsh|zsh)/","-c",$CMD,...) + - pattern: | + exec.CommandContext($CTX,"=~/(sh|bash|ksh|csh|tcsh|zsh)/","-c",$CMD,...) + - pattern-not: | + exec.Command("...","...","...",...) + - pattern-not: | + exec.CommandContext($CTX,"...","...","...",...) + - pattern-either: + - pattern: | + exec.Command("=~/\/bin\/env/","=~/(sh|bash|ksh|csh|tcsh|zsh)/","-c",$CMD,...) + - pattern: | + exec.CommandContext($CTX,"=~/\/bin\/env/","=~/(sh|bash|ksh|csh|tcsh|zsh)/","-c",$CMD,...) + - pattern-inside: | + import "os/exec" + ... + - pattern-not-inside: | + $CMD,$ERR := exec.LookPath("..."); + ... + - pattern-not-inside: | + $CMD = "..."; + ... + message: >- + Detected non-static command inside Command. Audit the input to 'exec.Command'. + If unverified user data can reach this call site, this is a code injection + vulnerability. A malicious actor can inject a malicious script to execute + arbitrary code. + metadata: + cwe: + - "CWE-94: Improper Control of Generation of Code ('Code Injection')" + owasp: + - A03:2021 - Injection + category: security + technology: + - go + confidence: LOW + references: + - https://owasp.org/Top10/A03_2021-Injection + cwe2022-top25: true + subcategory: + - audit + likelihood: LOW + impact: HIGH + severity: ERROR + languages: [go] diff --git a/go/lang/security/audit/dangerous-syscall-exec.go b/go/lang/security/audit/dangerous-syscall-exec.go new file mode 100644 index 00000000..ae5fc536 --- /dev/null +++ b/go/lang/security/audit/dangerous-syscall-exec.go @@ -0,0 +1,80 @@ +package main + +import "syscall" +import "os" +import "os/exec" + +func test1(userInput string) { + + binary, lookErr := exec.LookPath(userInput) + if lookErr != nil { + panic(lookErr) + } + + args := []string{"ls", "-a", "-l", "-h"} + + env := os.Environ() + + // ruleid:dangerous-syscall-exec + execErr := syscall.Exec(binary, args, env) + if execErr != nil { + panic(execErr) + } +} + + +func test2(userInput string) { + + binary, lookErr := exec.LookPath("sh") + if lookErr != nil { + panic(lookErr) + } + + args := []string{userInput, "-a", "-l", "-h"} + + env := os.Environ() + + // ruleid:dangerous-syscall-exec + execErr := syscall.Exec(binary, args, env) + if execErr != nil { + panic(execErr) + } +} + +func test3(userInput string) { + + binary, lookErr := exec.LookPath("sh") + if lookErr != nil { + panic(lookErr) + } + + args := []string{binary, "-c", userInput} + + env := os.Environ() + + // ruleid:dangerous-syscall-exec + execErr := syscall.Exec(binary, args, env) + if execErr != nil { + panic(execErr) + } +} + + + +func okTest1(userInput string) { + + binary, lookErr := exec.LookPath("ls") + if lookErr != nil { + panic(lookErr) + } + + args := []string{"ls", "-a", "-l", "-h"} + + env := os.Environ() + + // ok:dangerous-syscall-exec + execErr := syscall.Exec(binary, args, env) + if execErr != nil { + panic(execErr) + } +} diff --git a/go/lang/security/audit/dangerous-syscall-exec.yaml b/go/lang/security/audit/dangerous-syscall-exec.yaml new file mode 100644 index 00000000..1a66818b --- /dev/null +++ b/go/lang/security/audit/dangerous-syscall-exec.yaml @@ -0,0 +1,97 @@ +rules: +- id: dangerous-syscall-exec + patterns: + - pattern-either: + - patterns: + - pattern: | + syscall.$METHOD($BIN,...) + - pattern-not: | + syscall.$METHOD("...",...) + - pattern-not-inside: | + $BIN,$ERR := exec.LookPath("..."); + ... + - pattern-not-inside: | + $BIN = "..."; + ... + - patterns: + - pattern: | + syscall.$METHOD($BIN,$ARGS,...) + - pattern-not: | + syscall.$METHOD($BIN,[]string{"...",...},...) + - pattern-not-inside: | + $ARGS := []string{"...",...}; + ... + - pattern-not-inside: | + $CMD = "..."; + ... + $ARGS = []string{$CMD,...}; + ... + - pattern-not-inside: | + $CMD,$ERR := exec.LookPath("..."); + ... + $ARGS = []string{$CMD,...}; + ... + - patterns: + - pattern: | + syscall.$METHOD($BIN,[]string{"=~/(sh|bash|ksh|csh|tcsh|zsh)/","-c",$EXE,...},...) + - pattern-not: | + syscall.$METHOD($BIN,[]string{"...","...","...",...},...) + - patterns: + - pattern: | + syscall.$METHOD($BIN,$ARGS,...) + - pattern-either: + - pattern-inside: | + $ARGS := []string{"=~/(sh|bash|ksh|csh|tcsh|zsh)/","-c",$EXE,...}; + ... + - pattern-inside: | + $CMD = "=~/(sh|bash|ksh|csh|tcsh|zsh)/"; + ... + $ARGS = []string{$CMD,"-c",$EXE,...}; + ... + - pattern-inside: | + $CMD,$ERR := exec.LookPath("=~/(sh|bash|ksh|csh|tcsh|zsh)/"); + ... + $ARGS = []string{$CMD,"-c",$EXE,...}; + ... + - pattern-not-inside: | + $ARGS := []string{"...","...","...",...}; + ... + - pattern-not-inside: | + $CMD = "..."; + ... + $ARGS = []string{$CMD,"...","...",...}; + ... + - pattern-not-inside: | + $CMD,$ERR := exec.LookPath("..."); + ... + $ARGS = []string{$CMD,"...","...",...}; + ... + - pattern-inside: | + import "syscall" + ... + - metavariable-regex: + metavariable: $METHOD + regex: (Exec|ForkExec) + message: >- + Detected non-static command inside Exec. Audit the input to 'syscall.Exec'. + If unverified user data can reach this call site, this is a code injection + vulnerability. A malicious actor can inject a malicious script to execute + arbitrary code. + metadata: + cwe: + - "CWE-94: Improper Control of Generation of Code ('Code Injection')" + owasp: + - A03:2021 - Injection + category: security + technology: + - go + confidence: LOW + references: + - https://owasp.org/Top10/A03_2021-Injection + cwe2022-top25: true + subcategory: + - audit + likelihood: LOW + impact: HIGH + severity: ERROR + languages: [go] diff --git a/go/lang/security/audit/database/string-formatted-query.go b/go/lang/security/audit/database/string-formatted-query.go new file mode 100644 index 00000000..4197bde8 --- /dev/null +++ b/go/lang/security/audit/database/string-formatted-query.go @@ -0,0 +1,277 @@ +package main + +import ( + "context" + "database/sql" + "fmt" + "http" + + "github.com/jackc/pgx/v4" +) + +var db *sql.DB +var postgresDb *pgx.Conn + +func dbExec(r *http.Request) { + customerId := r.URL.Query().Get("id") + // ruleid: string-formatted-query + query := "SELECT number, expireDate, cvv FROM creditcards WHERE customerId = " + customerId + + row, _ := db.Exec(query) + + // ok: string-formatted-query + out, err := sshClient.Exec(fmt.Sprintf("sudo bash %s", scriptPath)) +} + +func okDbExec(r *http.Request) { + customerId := r.URL.Query().Get("id") + // ok: string-formatted-query + query := "SELECT number, expireDate, cvv FROM creditcards WHERE customerId = customerId" + + row, _ := db.Exec(query) +} + +func dbQuery1(r *http.Request) { + // ruleid: string-formatted-query + _, err = db.Query("INSERT into users (username, password) VALUES(" + username + ", " + password) + if err != nil { + http.Error("mistake") + } +} + +func dbQuery2(r *http.Request, username string, password string) { + // ruleid: string-formatted-query + query = "INSERT into users (username, password) VALUES(" + username + ", " + password + _, err = db.QueryRow(query) + if err != nil { + http.Error("mistake") + } +} + +func dbQuery3(r *http.Request, username string) { + // ruleid: string-formatted-query + query = username + " AND INSERT into users (username, password)" + _, err = db.Exec(query) + if err != nil { + http.Error("mistake") + } +} + +func dbQuery4(r *http.Request, username string) { + // ruleid: string-formatted-query + query := fmt.Sprintf("%s AND INSERT into users (username, password)", username) + _, err = db.Exec(query) + if err != nil { + http.Error("mistake") + } +} + +func dbQuery5(r *http.Request, username string, password string) { + // ruleid: string-formatted-query + query := fmt.Sprintf("INSERT into users (username, password) VALUES(%s, %s)", username, password) + _, err = db.QueryRow(query) + if err != nil { + http.Error("mistake") + } +} + +func okDbQuery1(r *http.Request) { + // ok: string-formatted-query + _, err = db.Exec("INSERT into users (username, password) VALUES(" + "username" + ", " + "smth)") + if err != nil { + http.Error("mistake") + } +} + +func dbExecContext(r *http.Request) { + ctx := context.Background() + customerId := r.URL.Query().Get("id") + // ruleid: string-formatted-query + query := "SELECT number, expireDate, cvv FROM creditcards WHERE customerId = " + customerId + + row, _ := db.ExecContext(ctx, query) +} + +func dbQuery4(r *http.Request) { + customerId := r.URL.Query().Get("id") + // ruleid: string-formatted-query + query := "SELECT number, expireDate, cvv FROM creditcards WHERE customerId = " + customerId + + row, _ := db.Query(query) +} + +func dbQueryContext(r *http.Request) { + ctx := context.Background() + customerId := r.URL.Query().Get("id") + // ruleid: string-formatted-query + query := "SELECT number, expireDate, cvv FROM creditcards WHERE customerId = " + customerId + + row, _ := db.QueryContext(ctx, query) +} + +func dbQueryRow(r *http.Request) { + customerId := r.URL.Query().Get("id") + // ruleid: string-formatted-query + query := "SELECT number, expireDate, cvv FROM creditcards WHERE customerId = " + customerId + + row, _ := db.QueryRow(query) +} + +func dbQueryRowContext(r *http.Request) { + ctx := context.Background() + customerId := r.URL.Query().Get("id") + // ruleid: string-formatted-query + query := "SELECT number, expireDate, cvv FROM creditcards WHERE customerId = " + customerId + + row, _ := db.QueryRowContext(ctx, query) +} + +func dbExecFmt(r *http.Request) { + customerId := r.URL.Query().Get("id") + query := "SELECT number, expireDate, cvv FROM creditcards WHERE customerId = %s" + // ruleid: string-formatted-query + query = fmt.Printf(query, customerId) + + row, _ := db.Exec(query) +} + +func dbExecContextFmt(r *http.Request) { + ctx := context.Background() + customerId := r.URL.Query().Get("id") + query := "SELECT number, expireDate, cvv FROM creditcards WHERE customerId = %s" + // ruleid: string-formatted-query + query = fmt.Printf(query, customerId) + + row, _ := db.ExecContext(ctx, query) +} + +func dbQueryFmt(r *http.Request) { + customerId := r.URL.Query().Get("id") + query := "SELECT number, expireDate, cvv FROM creditcards WHERE customerId = %s" + // ruleid: string-formatted-query + query = fmt.Printf(query, customerId) + + row, _ := db.Query(query) +} + +func dbQueryContextFmtReassign(r *http.Request) { + ctx := context.Background() + customerId := r.URL.Query().Get("id") + query := "SELECT number, expireDate, cvv FROM creditcards WHERE customerId = %s" + // ruleid: string-formatted-query + query = fmt.Printf(query, customerId) + + row, _ := db.QueryContext(ctx, query) +} + + +func dbQueryContextFmt(r *http.Request) { + ctx := context.Background() + customerId := r.URL.Query().Get("id") + // ruleid: string-formatted-query + query := fmt.Sprintf("SELECT number, expireDate, cvv FROM creditcards WHERE customerId = %s", customerId) + row, _ := db.QueryContext(ctx, query) +} + +func dbQueryRowFmt(r *http.Request) { + customerId := r.URL.Query().Get("id") + query := "SELECT number, expireDate, cvv FROM creditcards WHERE customerId = %s" + // ruleid: string-formatted-query + query = fmt.Printf(query, customerId) + + row, _ := db.QueryRow(query) +} + +func dbQueryRowContextReassign(r *http.Request) { + ctx := context.Background() + customerId := r.URL.Query().Get("id") + query := "SELECT number, expireDate, cvv FROM creditcards WHERE customerId = %s" + // ruleid: string-formatted-query + query = fmt.Printf(query, customerId) + + row, _ := db.QueryRowContext(ctx, query) +} + +func dbQueryRowContextFmt(r *http.Request) { + ctx := context.Background() + customerId := r.URL.Query().Get("id") + // ruleid: string-formatted-query + query := fmt.Sprintf("SELECT number, expireDate, cvv FROM creditcards WHERE customerId = %s", customerId) + + row, _ := db.QueryRowContext(ctx, query) +} + +func unmodifiedString() { + // ok: string-formatted-query + query := "SELECT number, expireDate, cvv FROM creditcards WHERE customerId = 1234" + row, _ := db.Query(query) +} + +func unmodifiedStringDirectly() { + // ok: string-formatted-query + row, _ := db.Query("SELECT number, expireDate, cvv FROM creditcards WHERE customerId = 1234") +} + +func badDirectQueryAdd(r *http.Request) { + ctx := context.Background() + customerId := r.URL.Query().Get("id") + + // ruleid: string-formatted-query + row, _ := db.QueryRowContext(ctx, "SELECT number, expireDate, cvv FROM creditcards WHERE customerId = " + customerId) +} + +func badDirectQueryFmt(r *http.Request) { + ctx := context.Background() + customerId := r.URL.Query().Get("id") + + // ruleid: string-formatted-query + row, _ := db.QueryRowContext(ctx, fmt.Printf("SELECT number, expireDate, cvv FROM creditcards WHERE customerId = %s", customerId)) +} + +func postgresBadDirectQueryFmt(r *http.Request) { + ctx := context.Background() + customerId := r.URL.Query().Get("id") + + // ruleid: string-formatted-query + row, _ := postgresDb.QueryRow(ctx, fmt.Printf("SELECT number, expireDate, cvv FROM creditcards WHERE customerId = %s", customerId)) +} + +func postgresQueryFmt(r *http.Request) { + ctx := context.Background() + customerId := r.URL.Query().Get("id") + // ruleid: string-formatted-query + query := fmt.Sprintf("SELECT number, expireDate, cvv FROM creditcards WHERE customerId = %s", customerId) + + row, _ := postgresDb.QueryRow(ctx, query) +} + +package main + +import ( + "context" + "database/sql" + "fmt" + "http" + + "github.com/jackc/pgx/v4" +) +// cf. https://github.com/returntocorp/semgrep-rules/issues/1249 +func new() { + // ok: string-formatted-query + var insertSql string = "insert into t_ad_experiment (exp_layer,buckets,opposite_buckets,is_transparent, " + + " description,is_full,start_time,end_time,creat_time,update_time,update_user,white_list,extra,status)" + + " value (?,?,?,?,?,?,?,?,?,?,?,?,?,?)" + t := time.Now().Unix() + InsertResult, err := DbConn.Exec(insertSql, info.Exp_layer, info.Buckets, info.Opposite_buckets, + info.Is_transparent, info.Description, info.Is_full, info.Start_time, info.End_time, t, t, + session.User, info.White_list, info.Extra, 0) +} + +func new2() { + // ok: string-formatted-query + var insertSql string = "insert into t_ad_experiment (exp_layer,buckets,opposite_buckets,is_transparent, description,is_full,start_time,end_time,creat_time,update_time,update_user,white_list,extra,status) value (?,?,?,?,?,?,?,?,?,?,?,?,?,?)" + t := time.Now().Unix() + InsertResult, err := DbConn.Exec(insertSql, info.Exp_layer, info.Buckets, info.Opposite_buckets, + info.Is_transparent, info.Description, info.Is_full, info.Start_time, info.End_time, t, t, + session.User, info.White_list, info.Extra, 0) +} diff --git a/go/lang/security/audit/database/string-formatted-query.yaml b/go/lang/security/audit/database/string-formatted-query.yaml new file mode 100644 index 00000000..7aeb388a --- /dev/null +++ b/go/lang/security/audit/database/string-formatted-query.yaml @@ -0,0 +1,107 @@ +rules: +- id: string-formatted-query + languages: [go] + message: >- + String-formatted SQL query detected. This could lead to SQL injection if + the string is not sanitized properly. Audit this call to ensure the + SQL is not manipulable by external data. + severity: WARNING + metadata: + owasp: + - A01:2017 - Injection + - A03:2021 - Injection + cwe: + - "CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')" + source-rule-url: https://github.com/securego/gosec + category: security + technology: + - go + confidence: LOW + references: + - https://owasp.org/Top10/A03_2021-Injection + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - audit + likelihood: LOW + impact: HIGH + patterns: + - metavariable-regex: + metavariable: $OBJ + regex: (?i).*(db|database) + - pattern-not-inside: | + $VAR = "..." + "..." + ... + $OBJ.$SINK(..., $VAR, ...) + - pattern-not: $OBJ.Exec("...") + - pattern-not: $OBJ.ExecContext($CTX, "...") + - pattern-not: $OBJ.Query("...") + - pattern-not: $OBJ.QueryContext($CTX, "...") + - pattern-not: $OBJ.QueryRow("...") + - pattern-not: $OBJ.QueryRow($CTX, "...") + - pattern-not: $OBJ.QueryRowContext($CTX, "...") + - pattern-either: + - pattern: $OBJ.Exec($X + ...) + - pattern: $OBJ.ExecContext($CTX, $X + ...) + - pattern: $OBJ.Query($X + ...) + - pattern: $OBJ.QueryContext($CTX, $X + ...) + - pattern: $OBJ.QueryRow($X + ...) + - pattern: $OBJ.QueryRow($CTX, $X + ...) + - pattern: $OBJ.QueryRowContext($CTX, $X + ...) + - pattern: $OBJ.Exec(fmt.$P("...", ...)) + - pattern: $OBJ.ExecContext($CTX, fmt.$P("...", ...)) + - pattern: $OBJ.Query(fmt.$P("...", ...)) + - pattern: $OBJ.QueryContext($CTX, fmt.$P("...", ...)) + - pattern: $OBJ.QueryRow(fmt.$P("...", ...)) + - pattern: $OBJ.QueryRow($CTX, fmt.$U("...", ...)) + - pattern: $OBJ.QueryRowContext($CTX, fmt.$P("...", ...)) + - patterns: + - pattern-either: + - pattern: $QUERY = fmt.Fprintf($F, "$SQLSTR", ...) + - pattern: $QUERY = fmt.Sprintf("$SQLSTR", ...) + - pattern: $QUERY = fmt.Printf("$SQLSTR", ...) + - pattern: $QUERY = $X + ... + - pattern-either: + - pattern-inside: | + func $FUNC(...) { + ... + $OBJ.Query($QUERY, ...) + ... + } + - pattern-inside: | + func $FUNC(...) { + ... + $OBJ.ExecContext($CTX, $QUERY, ...) + ... + } + - pattern-inside: | + func $FUNC(...) { + ... + $OBJ.Exec($QUERY, ...) + ... + } + - pattern-inside: | + func $FUNC(...) { + ... + $OBJ.QueryRow($CTX, $QUERY) + ... + } + - pattern-inside: | + func $FUNC(...) { + ... + $OBJ.QueryRow($QUERY) + ... + } + - pattern-inside: | + func $FUNC(...) { + ... + $OBJ.QueryContext($CTX, $QUERY) + ... + } + - pattern-inside: | + func $FUNC(...) { + ... + $OBJ.QueryRowContext($CTX, $QUERY, ...) + ... + } + \ No newline at end of file diff --git a/go/lang/security/audit/md5-used-as-password.go b/go/lang/security/audit/md5-used-as-password.go new file mode 100644 index 00000000..e1c29e27 --- /dev/null +++ b/go/lang/security/audit/md5-used-as-password.go @@ -0,0 +1,42 @@ +package main + +import ( + "crypto/md5" + "crypto/sha256" + "fmt" + "io" +) + +//// True positives //// +func ex1(user *User, pwtext string) { + h := md5.New() + io.WriteString(h, pwtext) + // ruleid: md5-used-as-password + user.setPassword(h.Sum(nil)) +} + +func ex2(user *User, pwtext string) { + data := []byte(pwtext) + // ruleid: md5-used-as-password + user.setPassword(md5.Sum(data)) +} + +//// True negatives //// +func ok1(user *User, pwtext string) { + h := sha256.New() + io.WriteString(h, pwtext) + // ok: md5-used-as-password + user.setPassword(h.Sum(nil)) +} + +func ok2(user *User, pwtext string) { + data := []byte(pwtext) + // ok: md5-used-as-password + user.setPassword(sha256.Sum(data)) +} + +func ok3(user *User, pwtext string) { + data := []byte(pwtext) + // ok: md5-used-as-password + user.setSomethingElse(md5.Sum(data)) +} diff --git a/go/lang/security/audit/md5-used-as-password.yaml b/go/lang/security/audit/md5-used-as-password.yaml new file mode 100644 index 00000000..b2d42f92 --- /dev/null +++ b/go/lang/security/audit/md5-used-as-password.yaml @@ -0,0 +1,43 @@ +rules: +- id: md5-used-as-password + languages: [go] + severity: WARNING + message: >- + It looks like MD5 is used as a password hash. MD5 is not considered a + secure password hash because it can be cracked by an attacker in a short + amount of time. Use a suitable password hashing function such as bcrypt. + You can use the `golang.org/x/crypto/bcrypt` package. + options: + interfile: true + metadata: + category: security + technology: + - md5 + references: + - https://tools.ietf.org/id/draft-lvelvindron-tls-md5-sha1-deprecate-01.html + - https://security.stackexchange.com/questions/211/how-to-securely-hash-passwords + - https://github.com/returntocorp/semgrep-rules/issues/1609 + - https://pkg.go.dev/golang.org/x/crypto/bcrypt + owasp: + - A03:2017 - Sensitive Data Exposure + - A02:2021 - Cryptographic Failures + cwe: + - 'CWE-327: Use of a Broken or Risky Cryptographic Algorithm' + confidence: MEDIUM + subcategory: + - vuln + likelihood: MEDIUM + impact: MEDIUM + interfile: true + mode: taint + pattern-sources: + - patterns: + - pattern-either: + - pattern: md5.New + - pattern: md5.Sum + pattern-sinks: + - patterns: + - pattern: $FUNCTION(...) + - metavariable-regex: + metavariable: $FUNCTION + regex: (?i)(.*password.*) diff --git a/go/lang/security/audit/net/bind_all.go b/go/lang/security/audit/net/bind_all.go new file mode 100644 index 00000000..38b2708e --- /dev/null +++ b/go/lang/security/audit/net/bind_all.go @@ -0,0 +1,33 @@ +package main + +import ( + "log" + "net" +) + +func bind_all() { + // ruleid: avoid-bind-to-all-interfaces + l, err := net.Listen("tcp", "0.0.0.0:2000") + if err != nil { + log.Fatal(err) + } + defer l.Close() +} + +func bind_default() { + // ruleid: avoid-bind-to-all-interfaces + l, err := net.Listen("tcp", ":2000") + if err != nil { + log.Fatal(err) + } + defer l.Close() +} + +func main() { + // ok: avoid-bind-to-all-interfaces + l, err := net.Listen("tcp", "192.168.1.101:2000") + if err != nil { + log.Fatal(err) + } + defer l.Close() +} diff --git a/go/lang/security/audit/net/bind_all.yaml b/go/lang/security/audit/net/bind_all.yaml new file mode 100644 index 00000000..967ffb5d --- /dev/null +++ b/go/lang/security/audit/net/bind_all.yaml @@ -0,0 +1,30 @@ +rules: +- id: avoid-bind-to-all-interfaces + message: >- + Detected a network listener listening on 0.0.0.0 or an empty string. This could unexpectedly expose + the server publicly as it binds to all available interfaces. Instead, specify another IP address + that is not 0.0.0.0 nor the empty string. + languages: [go] + severity: WARNING + metadata: + cwe: + - 'CWE-200: Exposure of Sensitive Information to an Unauthorized Actor' + owasp: + - A01:2021 - Broken Access Control + source-rule-url: https://github.com/securego/gosec + category: security + technology: + - go + confidence: HIGH + references: + - https://owasp.org/Top10/A01_2021-Broken_Access_Control + cwe2021-top25: true + subcategory: + - audit + likelihood: LOW + impact: MEDIUM + pattern-either: + - pattern: tls.Listen($NETWORK, "=~/^0.0.0.0:.*$/", ...) + - pattern: net.Listen($NETWORK, "=~/^0.0.0.0:.*$/", ...) + - pattern: tls.Listen($NETWORK, "=~/^:.*$/", ...) + - pattern: net.Listen($NETWORK, "=~/^:.*$/", ...) diff --git a/go/lang/security/audit/net/bind_all_default.go b/go/lang/security/audit/net/bind_all_default.go new file mode 100644 index 00000000..7ad2f2fe --- /dev/null +++ b/go/lang/security/audit/net/bind_all_default.go @@ -0,0 +1,15 @@ +package main + +import ( + "log" + "net" +) + +func main() { + // ruleid: avoid-bind-to-all-interfaces + l, err := net.Listen("tcp", ":2000") + if err != nil { + log.Fatal(err) + } + defer l.Close() +} diff --git a/go/lang/security/audit/net/cookie-missing-httponly.go b/go/lang/security/audit/net/cookie-missing-httponly.go new file mode 100644 index 00000000..84e5dbf7 --- /dev/null +++ b/go/lang/security/audit/net/cookie-missing-httponly.go @@ -0,0 +1,68 @@ +// cf. https://github.com/0c34/govwa/blob/139693e56406b5684d2a6ae22c0af90717e149b8/util/cookie.go + +package main + +import ( + "net/http" + "time" +) + +func SetCookieLevel(w http.ResponseWriter, r *http.Request, cookievalue string) { + + level := cookievalue + if level == "" { + level = "low" + } + SetCookie(w, "Level", level) + +} + +func CheckLevel(r *http.Request) bool { + level := GetCookie(r, "Level") + if level == "" || level == "low" { + return false //set default level to low + } else if level == "high" { + return true //level == high + } else { + return false // level == low + } +} + +/* cookie setter getter */ + +func SetCookie(w http.ResponseWriter, name, value string) { + // ruleid: cookie-missing-httponly + cookie := http.Cookie{ + Name: name, + Value: value, + } + http.SetCookie(w, &cookie) +} + +func SetSecureCookie(w http.ResponseWriter, name, value string) { + // ok: cookie-missing-httponly + cookie := http.Cookie{ + Secure: true, + HttpOnly: true, + Name: name, + Value: value, + } + http.SetCookie(w, &cookie) +} + +func GetCookie(r *http.Request, name string) string { + cookie, _ := r.Cookie(name) + return cookie.Value +} + +func DeleteCookie(w http.ResponseWriter, cookies []string) { + for _, name := range cookies { + // ruleid: cookie-missing-httponly + cookie := &http.Cookie{ + Name: name, + Value: "", + Expires: time.Unix(0, 0), + } + http.SetCookie(w, cookie) + } +} diff --git a/go/lang/security/audit/net/cookie-missing-httponly.yaml b/go/lang/security/audit/net/cookie-missing-httponly.yaml new file mode 100644 index 00000000..64f045c2 --- /dev/null +++ b/go/lang/security/audit/net/cookie-missing-httponly.yaml @@ -0,0 +1,40 @@ +rules: +- id: cookie-missing-httponly + patterns: + - pattern-not-inside: | + http.Cookie{ + ..., + HttpOnly: true, + ..., + } + - pattern: | + http.Cookie{ + ..., + } + message: >- + A session cookie was detected without setting the 'HttpOnly' flag. + The 'HttpOnly' flag for cookies instructs the browser to forbid + client-side scripts from reading the cookie which mitigates XSS + attacks. Set the 'HttpOnly' flag by setting 'HttpOnly' to 'true' + in the Cookie. + metadata: + cwe: + - "CWE-1004: Sensitive Cookie Without 'HttpOnly' Flag" + owasp: + - A05:2021 - Security Misconfiguration + references: + - https://github.com/0c34/govwa/blob/139693e56406b5684d2a6ae22c0af90717e149b8/util/cookie.go + - https://golang.org/src/net/http/cookie.go + category: security + technology: + - go + confidence: MEDIUM + subcategory: + - vuln + likelihood: LOW + impact: LOW + fix-regex: + regex: (HttpOnly\s*:\s+)false + replacement: \1true + severity: WARNING + languages: [go] diff --git a/go/lang/security/audit/net/cookie-missing-secure.go b/go/lang/security/audit/net/cookie-missing-secure.go new file mode 100644 index 00000000..695eca45 --- /dev/null +++ b/go/lang/security/audit/net/cookie-missing-secure.go @@ -0,0 +1,68 @@ +// cf. https://github.com/0c34/govwa/blob/139693e56406b5684d2a6ae22c0af90717e149b8/util/cookie.go + +package main + +import ( + "net/http" + "time" +) + +func SetCookieLevel(w http.ResponseWriter, r *http.Request, cookievalue string) { + + level := cookievalue + if level == "" { + level = "low" + } + SetCookie(w, "Level", level) + +} + +func CheckLevel(r *http.Request) bool { + level := GetCookie(r, "Level") + if level == "" || level == "low" { + return false //set default level to low + } else if level == "high" { + return true //level == high + } else { + return false // level == low + } +} + +/* cookie setter getter */ + +func SetCookie(w http.ResponseWriter, name, value string) { + // ruleid: cookie-missing-secure + cookie := http.Cookie{ + Name: name, + Value: value, + } + http.SetCookie(w, &cookie) +} + +func SetSecureCookie(w http.ResponseWriter, name, value string) { + // ok: cookie-missing-secure + cookie := http.Cookie{ + Secure: true, + HttpOnly: true, + Name: name, + Value: value, + } + http.SetCookie(w, &cookie) +} + +func GetCookie(r *http.Request, name string) string { + cookie, _ := r.Cookie(name) + return cookie.Value +} + +func DeleteCookie(w http.ResponseWriter, cookies []string) { + for _, name := range cookies { + // ruleid: cookie-missing-secure + cookie := &http.Cookie{ + Name: name, + Value: "", + Expires: time.Unix(0, 0), + } + http.SetCookie(w, cookie) + } +} diff --git a/go/lang/security/audit/net/cookie-missing-secure.yaml b/go/lang/security/audit/net/cookie-missing-secure.yaml new file mode 100644 index 00000000..88d58eca --- /dev/null +++ b/go/lang/security/audit/net/cookie-missing-secure.yaml @@ -0,0 +1,39 @@ +rules: +- id: cookie-missing-secure + patterns: + - pattern-not-inside: | + http.Cookie{ + ..., + Secure: true, + ..., + } + - pattern: | + http.Cookie{ + ..., + } + message: >- + A session cookie was detected without setting the 'Secure' flag. + The 'secure' flag for cookies prevents the client from transmitting + the cookie over insecure channels such as HTTP. Set the 'Secure' + flag by setting 'Secure' to 'true' in the Options struct. + metadata: + cwe: + - "CWE-614: Sensitive Cookie in HTTPS Session Without 'Secure' Attribute" + owasp: + - A05:2021 - Security Misconfiguration + references: + - https://github.com/0c34/govwa/blob/139693e56406b5684d2a6ae22c0af90717e149b8/util/cookie.go + - https://golang.org/src/net/http/cookie.go + category: security + technology: + - go + confidence: MEDIUM + subcategory: + - vuln + likelihood: LOW + impact: LOW + fix-regex: + regex: (Secure\s*:\s+)false + replacement: \1true + severity: WARNING + languages: [go] \ No newline at end of file diff --git a/go/lang/security/audit/net/dynamic-httptrace-clienttrace-ok.go b/go/lang/security/audit/net/dynamic-httptrace-clienttrace-ok.go new file mode 100644 index 00000000..8bc0eee8 --- /dev/null +++ b/go/lang/security/audit/net/dynamic-httptrace-clienttrace-ok.go @@ -0,0 +1,341 @@ +/* + * Test case reference: + * cf. https://github.com/containous/traefik//blob/bb4de11c517dfa4a6f6ca446732f4b55f771cb49/pkg/middlewares/retry/retry.go + */ + +package main + +import ( + "bufio" + "context" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/http/httptrace" + "time" + + "github.com/containous/traefik/v2/pkg/config/dynamic" + "github.com/containous/traefik/v2/pkg/log" + "github.com/containous/traefik/v2/pkg/middlewares" + "github.com/containous/traefik/v2/pkg/tracing" + "github.com/opentracing/opentracing-go/ext" +) + +// Compile time validation that the response writer implements http interfaces correctly. +var _ middlewares.Stateful = &responseWriterWithCloseNotify{} + +const ( + typeName = "Retry" +) + +// Listener is used to inform about retry attempts. +type Listener interface { + // Retried will be called when a retry happens, with the request attempt passed to it. + // For the first retry this will be attempt 2. + Retried(req *http.Request, attempt int) +} + +// Listeners is a convenience type to construct a list of Listener and notify +// each of them about a retry attempt. +type Listeners []Listener + +// retry is a middleware that retries requests. +type retry struct { + attempts int + next http.Handler + listener Listener + name string +} + +// New returns a new retry middleware. +func New(ctx context.Context, next http.Handler, config dynamic.Retry, listener Listener, name string) (http.Handler, error) { + log.FromContext(middlewares.GetLoggerCtx(ctx, name, typeName)).Debug("Creating middleware") + + if config.Attempts <= 0 { + return nil, fmt.Errorf("incorrect (or empty) value for attempt (%d)", config.Attempts) + } + + return &retry{ + attempts: config.Attempts, + next: next, + listener: listener, + name: name, + }, nil +} + +func (r *retry) GetTracingInformation() (string, ext.SpanKindEnum) { + return r.name, tracing.SpanKindNoneEnum +} + +func (r *retry) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + // if we might make multiple attempts, swap the body for an ioutil.NopCloser + // cf https://github.com/containous/traefik/issues/1008 + if r.attempts > 1 { + body := req.Body + defer body.Close() + req.Body = ioutil.NopCloser(body) + } + + attempts := 1 + for { + shouldRetry := attempts < r.attempts + retryResponseWriter := newResponseWriter(rw, shouldRetry) + + // Disable retries when the backend already received request data + trace := &httptrace.ClientTrace{ + WroteHeaders: func() { + retryResponseWriter.DisableRetries() + }, + WroteRequest: func(httptrace.WroteRequestInfo) { + retryResponseWriter.DisableRetries() + }, + } + // ok: dynamic-httptrace-clienttrace + newCtx := httptrace.WithClientTrace(req.Context(), trace) + + r.next.ServeHTTP(retryResponseWriter, req.WithContext(newCtx)) + + if !retryResponseWriter.ShouldRetry() { + break + } + + attempts++ + + log.FromContext(middlewares.GetLoggerCtx(req.Context(), r.name, typeName)). + Debugf("New attempt %d for request: %v", attempts, req.URL) + + r.listener.Retried(req, attempts) + } +} + +// Retried exists to implement the Listener interface. It calls Retried on each of its slice entries. +func (l Listeners) Retried(req *http.Request, attempt int) { + for _, listener := range l { + listener.Retried(req, attempt) + } +} + +type responseWriter interface { + http.ResponseWriter + http.Flusher + ShouldRetry() bool + DisableRetries() +} + +func newResponseWriter(rw http.ResponseWriter, shouldRetry bool) responseWriter { + responseWriter := &responseWriterWithoutCloseNotify{ + responseWriter: rw, + headers: make(http.Header), + shouldRetry: shouldRetry, + } + if _, ok := rw.(http.CloseNotifier); ok { + return &responseWriterWithCloseNotify{ + responseWriterWithoutCloseNotify: responseWriter, + } + } + return responseWriter +} + +type responseWriterWithoutCloseNotify struct { + responseWriter http.ResponseWriter + headers http.Header + shouldRetry bool + written bool +} + +func (r *responseWriterWithoutCloseNotify) ShouldRetry() bool { + return r.shouldRetry +} + +func (r *responseWriterWithoutCloseNotify) DisableRetries() { + r.shouldRetry = false +} + +func (r *responseWriterWithoutCloseNotify) Header() http.Header { + if r.written { + return r.responseWriter.Header() + } + return r.headers +} + +func (r *responseWriterWithoutCloseNotify) Write(buf []byte) (int, error) { + if r.ShouldRetry() { + return len(buf), nil + } + return r.responseWriter.Write(buf) +} + +func (r *responseWriterWithoutCloseNotify) WriteHeader(code int) { + if r.ShouldRetry() && code == http.StatusServiceUnavailable { + // We get a 503 HTTP Status Code when there is no backend server in the pool + // to which the request could be sent. Also, note that r.ShouldRetry() + // will never return true in case there was a connection established to + // the backend server and so we can be sure that the 503 was produced + // inside Traefik already and we don't have to retry in this cases. + r.DisableRetries() + } + + if r.ShouldRetry() { + return + } + + // In that case retry case is set to false which means we at least managed + // to write headers to the backend : we are not going to perform any further retry. + // So it is now safe to alter current response headers with headers collected during + // the latest try before writing headers to client. + headers := r.responseWriter.Header() + for header, value := range r.headers { + headers[header] = value + } + + r.responseWriter.WriteHeader(code) + r.written = true +} + +func (r *responseWriterWithoutCloseNotify) Hijack() (net.Conn, *bufio.ReadWriter, error) { + hijacker, ok := r.responseWriter.(http.Hijacker) + if !ok { + return nil, nil, fmt.Errorf("%T is not a http.Hijacker", r.responseWriter) + } + return hijacker.Hijack() +} + +func (r *responseWriterWithoutCloseNotify) Flush() { + if flusher, ok := r.responseWriter.(http.Flusher); ok { + flusher.Flush() + } +} + +type responseWriterWithCloseNotify struct { + *responseWriterWithoutCloseNotify +} + +func (r *responseWriterWithCloseNotify) CloseNotify() <-chan bool { + return r.responseWriter.(http.CloseNotifier).CloseNotify() +} + +/* + * Test case reference + * cf. https://github.com/gocolly/colly/blob/b1a8ed2f18144f4b70abcfc18a5e58c68a062389/http_trace.go + */ + +// HTTPTrace provides a datastructure for storing an http trace. +type HTTPTrace struct { + start, connect time.Time + ConnectDuration time.Duration + FirstByteDuration time.Duration +} + +// trace returns a httptrace.ClientTrace object to be used with an http +// request via httptrace.WithClientTrace() that fills in the HttpTrace. +func (ht *HTTPTrace) trace() *httptrace.ClientTrace { + trace := &httptrace.ClientTrace{ + ConnectStart: func(network, addr string) { ht.connect = time.Now() }, + ConnectDone: func(network, addr string, err error) { + ht.ConnectDuration = time.Since(ht.connect) + }, + + GetConn: func(hostPort string) { ht.start = time.Now() }, + GotFirstResponseByte: func() { + ht.FirstByteDuration = time.Since(ht.start) + }, + } + return trace +} + +// WithTrace returns the given HTTP Request with this HTTPTrace added to its +// context. +func (ht *HTTPTrace) WithTrace(req *http.Request) *http.Request { + // ok: dynamic-httptrace-clienttrace + return req.WithContext(httptrace.WithClientTrace(req.Context(), ht.trace())) +} + +/* + * Test case reference + * cf. https://github.com/mehrdadrad/mylg//blob/616fd5309bb143d3f52ef866b2ffe12135f0dd4e/http/ping/ping.go + */ + +// Ping tries to ping a web server through http +func (p *Ping) Ping() (Result, error) { + var ( + r Result + sTime time.Time + resp *http.Response + req *http.Request + err error + ) + + client := &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + // Don't follow redirects + return http.ErrUseLastResponse + }, + Timeout: p.timeout, + Transport: p.transport, + } + + sTime = time.Now() + + if p.method == "POST" { + r.Size = len(p.buf) + reader := strings.NewReader(p.buf) + req, err = http.NewRequest(p.method, p.url, reader) + } else { + req, err = http.NewRequest(p.method, p.url, nil) + } + + if err != nil { + return r, err + } + + // customized header + req.Header.Add("User-Agent", p.uAgent) + // context, tracert + if p.tracerEnabled && !p.quiet { + // ok: dynamic-httptrace-clienttrace + req = req.WithContext(httptrace.WithClientTrace(req.Context(), tracer(&r))) + } + resp, err = client.Do(req) + + if err != nil { + return r, err + } + defer resp.Body.Close() + + r.TotalTime = time.Since(sTime).Seconds() + + if p.method == "GET" { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return r, err + } + r.Size = len(body) + } else { + io.Copy(ioutil.Discard, resp.Body) + } + + r.StatusCode = resp.StatusCode + r.Proto = resp.Proto + return r, nil +} + +func tracer(r *Result) *httptrace.ClientTrace { + var ( + begin = time.Now() + elapsed time.Duration + ) + + return &httptrace.ClientTrace{ + ConnectDone: func(network, addr string, err error) { + elapsed = time.Since(begin) + begin = time.Now() + r.Trace.ConnectionTime = elapsed.Seconds() * 1e3 + }, + GotFirstResponseByte: func() { + elapsed = time.Since(begin) + begin = time.Now() + r.Trace.TimeToFirstByte = elapsed.Seconds() * 1e3 + }, + } +} diff --git a/go/lang/security/audit/net/dynamic-httptrace-clienttrace.go b/go/lang/security/audit/net/dynamic-httptrace-clienttrace.go new file mode 100644 index 00000000..bd20b33e --- /dev/null +++ b/go/lang/security/audit/net/dynamic-httptrace-clienttrace.go @@ -0,0 +1,11 @@ +package main + +import ( + "net/http" + "net/http/httptrace" +) + +func WithTrace(req *http.Request, trace *httptrace.ClientTrace) *http.Request { + // ruleid: dynamic-httptrace-clienttrace + return req.WithContext(httptrace.WithClientTrace(req.Context(), trace)) +} diff --git a/go/lang/security/audit/net/dynamic-httptrace-clienttrace.yaml b/go/lang/security/audit/net/dynamic-httptrace-clienttrace.yaml new file mode 100644 index 00000000..36901286 --- /dev/null +++ b/go/lang/security/audit/net/dynamic-httptrace-clienttrace.yaml @@ -0,0 +1,38 @@ +rules: +- id: dynamic-httptrace-clienttrace + message: >- + Detected a potentially dynamic ClientTrace. This occurred because semgrep could + not + find a static definition for '$TRACE'. Dynamic ClientTraces are dangerous because + they deserialize function code to run when certain Request events occur, which + could lead + to code being run without your knowledge. Ensure that your ClientTrace is statically + defined. + metadata: + cwe: + - 'CWE-913: Improper Control of Dynamically-Managed Code Resources' + owasp: + - A01:2021 - Broken Access Control + references: + - https://github.com/returntocorp/semgrep-rules/issues/518 + # Detects when a static ClientTrace is not defined in the same file as + # WithClientTrace. Not a perfect detection, but sufficiently works in a + # scan of ~1k repos: https://dev.massive.ret2.co/triager/filter/1007 + category: security + technology: + - go + confidence: MEDIUM + subcategory: + - vuln + likelihood: LOW + impact: LOW + patterns: + - pattern-not-inside: | + package $PACKAGE + ... + &httptrace.ClientTrace { ... } + ... + - pattern: httptrace.WithClientTrace($ANY, $TRACE) + severity: WARNING + languages: + - go diff --git a/go/lang/security/audit/net/formatted-template-string.go b/go/lang/security/audit/net/formatted-template-string.go new file mode 100644 index 00000000..f7a35d88 --- /dev/null +++ b/go/lang/security/audit/net/formatted-template-string.go @@ -0,0 +1,75 @@ +package main + +import ( + "fmt" + "html/template" + "net/http" + "strconv" +) + +func Fine(r *http.Request) template.HTML { + // ok: formatted-template-string + return template.HTML("

Hello, world

") +} + +func AlsoFine(r *http.Request) template.HTML { + // ok: formatted-template-string + return template.HTML("

" + "Hello, world

") +} + +func Concat(r *http.Request) template.HTML { + customerId := r.URL.Query().Get("id") + // ruleid: formatted-template-string + tmpl := "

" + customerId + "

" + + return template.HTML(tmpl) +} + +func ConcatBranch(r *http.Request) template.HTML { + customerId := r.URL.Query().Get("id") + doIt, err := strconv.ParseBool(r.URL.Query().Get("do")) + if err != nil { + return template.HTML("") + } + var tmpl string + if doIt { + // todo: formatted-template-string + tmpl = "

" + customerId + "

" + } else { + tmpl = "" + } + + return template.HTML(tmpl) +} + +func ConcatInline(r *http.Request) template.HTML { + customerId := r.URL.Query().Get("id") + + // ruleid: formatted-template-string + return template.HTML("

" + customerId + "

") +} + +func ConcatInlineOneside(r *http.Request) template.HTML { + customerId := r.URL.Query().Get("id") + + // ruleid: formatted-template-string + return template.HTML("

" + customerId) +} + +func Formatted(r *http.Request) template.HTML { + customerId := r.URL.Query().Get("id") + // ruleid: formatted-template-string + tmpl, err := fmt.Printf("

%s

", customerId) + if err != nil { + return template.HTML("") + } + return template.HTML(tmpl) +} + +func FormattedInline(r *http.Request) template.HTML { + customerId := r.URL.Query().Get("id") + // ruleid: formatted-template-string + return template.HTML(fmt.Sprintf("

%s

", customerId)) +} + +func main() {} diff --git a/go/lang/security/audit/net/formatted-template-string.yaml b/go/lang/security/audit/net/formatted-template-string.yaml new file mode 100644 index 00000000..1a804a24 --- /dev/null +++ b/go/lang/security/audit/net/formatted-template-string.yaml @@ -0,0 +1,55 @@ +rules: +- id: formatted-template-string + message: >- + Found a formatted template string passed to 'template.HTML()'. 'template.HTML()' does not escape + contents. Be absolutely sure there is no user-controlled data in this template. If user data can + reach this template, you may have a XSS vulnerability. + metadata: + cwe: + - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')" + owasp: + - A07:2017 - Cross-Site Scripting (XSS) + - A03:2021 - Injection + references: + - https://golang.org/pkg/html/template/#HTML + category: security + technology: + - go + confidence: MEDIUM + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - audit + likelihood: LOW + impact: MEDIUM + languages: [go] + severity: WARNING + patterns: + - pattern-not: template.HTML("..." + "...") + - pattern-either: + - pattern: template.HTML($T + $X, ...) + - pattern: template.HTML(fmt.$P("...", ...), ...) + - pattern: | + $T = "..." + ... + $T = $FXN(..., $T, ...) + ... + template.HTML($T, ...) + - pattern: | + $T = fmt.$P("...", ...) + ... + template.HTML($T, ...) + - pattern: | + $T, $ERR = fmt.$P("...", ...) + ... + template.HTML($T, ...) + - pattern: | + $T = $X + $Y + ... + template.HTML($T, ...) + - pattern: |- + $T = "..." + ... + $OTHER, $ERR = fmt.$P(..., $T, ...) + ... + template.HTML($OTHER, ...) diff --git a/go/lang/security/audit/net/fs-directory-listing.go b/go/lang/security/audit/net/fs-directory-listing.go new file mode 100644 index 00000000..98ec97d5 --- /dev/null +++ b/go/lang/security/audit/net/fs-directory-listing.go @@ -0,0 +1,49 @@ +package main + +import ( + "log" + "net/http" +) + +func dirListing1() { + fs := http.FileServer(http.Dir("")) + //ruleid: fs-directory-listing + log.Fatal(http.ListenAndServe(":9000", fs)) +} + +func dirListing2() { + fs := http.FileServer(http.Dir("")) + certFile := "/path/tp/my/cert" + keyFile := "/path/to/my/key" + //ruleid: fs-directory-listing + log.Fatal(http.ListenAndServeTLS(":9000", certFile, keyFile, fs)) +} + +func dirListing3() { + fs := http.FileServer(http.Dir("")) + //ruleid: fs-directory-listing + http.Handle("/myroute", fs) +} + +func dirListing4() { + //ruleid: fs-directory-listing + http.Handle("/myroute", http.FileServer(http.Dir(""))) +} + +func noDirListing1() { + h1 := func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte("

Hello!

")) + } + //ok: fs-directory-listing + http.HandleFunc("/myroute", h1) +} + +func noDirListing2() { + h1 := func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte("

Home page

")) + } + mux := http.NewServeMux() + mux.HandleFunc("/", h1) + //ok: fs-directory-listing + log.Fatal(http.ListenAndServe(":9000", mux)) +} diff --git a/go/lang/security/audit/net/fs-directory-listing.yaml b/go/lang/security/audit/net/fs-directory-listing.yaml new file mode 100644 index 00000000..8cd4e68a --- /dev/null +++ b/go/lang/security/audit/net/fs-directory-listing.yaml @@ -0,0 +1,48 @@ +rules: +- id: fs-directory-listing + message: >- + Detected usage of 'http.FileServer' as handler: this allows directory listing + and an attacker could navigate through directories looking for sensitive + files. Be sure to disable directory listing or restrict access to specific + directories/files. + severity: WARNING + languages: + - go + patterns: + - pattern-either: + - patterns: + - pattern-inside: | + $FS := http.FileServer(...) + ... + - pattern-either: + - pattern: | + http.ListenAndServe(..., $FS) + - pattern: | + http.ListenAndServeTLS(..., $FS) + - pattern: | + http.Handle(..., $FS) + - pattern: | + http.HandleFunc(..., $FS) + - patterns: + - pattern: | + http.$FN(..., http.FileServer(...)) + - metavariable-regex: + metavariable: $FN + regex: (ListenAndServe|ListenAndServeTLS|Handle|HandleFunc) + metadata: + category: security + cwe: + - 'CWE-548: Exposure of Information Through Directory Listing' + owasp: + - A06:2017 - Security Misconfiguration + - A01:2021 - Broken Access Control + references: + - https://github.com/OWASP/Go-SCP + - https://cwe.mitre.org/data/definitions/548.html + confidence: MEDIUM + technology: + - go + subcategory: + - vuln + likelihood: MEDIUM + impact: MEDIUM diff --git a/go/lang/security/audit/net/pprof.go b/go/lang/security/audit/net/pprof.go new file mode 100644 index 00000000..45f193d4 --- /dev/null +++ b/go/lang/security/audit/net/pprof.go @@ -0,0 +1,43 @@ +package main + +import ( + "fmt" + "log" + "net/http" + + _ "net/http/pprof" +) + +func ok() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello World!") + }) + // ok: pprof-debug-exposure + log.Fatal(http.ListenAndServe("localhost:8080", nil)) +} + +func ok2() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello World!") + }) + // ok: pprof-debug-exposure + log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil)) +} + +func ok3() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello World!") + }) + + mux := http.NewServeMux() + // ok: pprof-debug-exposure + log.Fatal(http.ListenAndServe(":8080", mux)) +} + +func main() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello World!") + }) + // ruleid: pprof-debug-exposure + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/go/lang/security/audit/net/pprof.yaml b/go/lang/security/audit/net/pprof.yaml new file mode 100644 index 00000000..be595d45 --- /dev/null +++ b/go/lang/security/audit/net/pprof.yaml @@ -0,0 +1,40 @@ +rules: +- id: pprof-debug-exposure + metadata: + cwe: + - 'CWE-489: Active Debug Code' + owasp: 'A06:2017 - Security Misconfiguration' + source-rule-url: https://github.com/securego/gosec#available-rules + references: + - https://www.farsightsecurity.com/blog/txt-record/go-remote-profiling-20161028/ + category: security + technology: + - go + confidence: LOW + subcategory: + - audit + likelihood: LOW + impact: LOW + message: >- + The profiling 'pprof' endpoint is automatically exposed on /debug/pprof. + This could leak information about the server. + Instead, use `import "net/http/pprof"`. See + https://www.farsightsecurity.com/blog/txt-record/go-remote-profiling-20161028/ + for more information and mitigation. + languages: [go] + severity: WARNING + patterns: + - pattern-inside: | + import _ "net/http/pprof" + ... + - pattern-inside: | + func $ANY(...) { + ... + } + - pattern-not-inside: | + $MUX = http.NewServeMux(...) + ... + http.ListenAndServe($ADDR, $MUX) + - pattern-not: http.ListenAndServe("=~/^localhost.*/", ...) + - pattern-not: http.ListenAndServe("=~/^127[.]0[.]0[.]1.*/", ...) + - pattern: http.ListenAndServe(...) diff --git a/go/lang/security/audit/net/pprof_good.go b/go/lang/security/audit/net/pprof_good.go new file mode 100644 index 00000000..748b637e --- /dev/null +++ b/go/lang/security/audit/net/pprof_good.go @@ -0,0 +1,18 @@ +package main + +import ( + "fmt" + "log" + "net/http" + + // ok: pprof-debug-exposure + "net/http/pprof" +) + +func main() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello World!") + }) + pprof.StartCPUProfile() + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/go/lang/security/audit/net/pprof_good2.go b/go/lang/security/audit/net/pprof_good2.go new file mode 100644 index 00000000..7c391df2 --- /dev/null +++ b/go/lang/security/audit/net/pprof_good2.go @@ -0,0 +1,17 @@ +package main + +import ( + "fmt" + "log" + "net/http" + + // OK + _ "net/http/pprof" +) + +func main() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello World!") + }) + log.Fatal(http.ListenAndServe("localhost:8080", nil)) +} diff --git a/go/lang/security/audit/net/unescaped-data-in-htmlattr.go b/go/lang/security/audit/net/unescaped-data-in-htmlattr.go new file mode 100644 index 00000000..4e9bb902 --- /dev/null +++ b/go/lang/security/audit/net/unescaped-data-in-htmlattr.go @@ -0,0 +1,15 @@ +package main + +import ( + "html/template" + "net/http" +) + +const tmpl = "" + +func Concat(r *http.Request) template.HTML { + customerId := r.URL.Query().Get("id") + // ruleid: unescaped-data-in-htmlattr + tmpl := "

" + customerId + "

" + return template.HTMLAttr(tmpl) +} diff --git a/go/lang/security/audit/net/unescaped-data-in-htmlattr.yaml b/go/lang/security/audit/net/unescaped-data-in-htmlattr.yaml new file mode 100644 index 00000000..bb87d089 --- /dev/null +++ b/go/lang/security/audit/net/unescaped-data-in-htmlattr.yaml @@ -0,0 +1,53 @@ +rules: +- id: unescaped-data-in-htmlattr + message: >- + Found a formatted template string passed to 'template. + HTMLAttr()'. 'template.HTMLAttr()' does not escape contents. Be absolutely sure there is no user-controlled + data in this template or validate and sanitize the data before passing it into the template. + metadata: + cwe: + - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')" + owasp: + - A07:2017 - Cross-Site Scripting (XSS) + - A03:2021 - Injection + references: + - https://golang.org/pkg/html/template/#HTMLAttr + category: security + technology: + - go + confidence: LOW + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - audit + likelihood: LOW + impact: MEDIUM + languages: [go] + severity: WARNING + pattern-either: + - pattern: template.HTMLAttr($T + $X, ...) + - pattern: template.HTMLAttr(fmt.$P("...", ...), ...) + - pattern: | + $T = "..." + ... + $T = $FXN(..., $T, ...) + ... + template.HTMLAttr($T, ...) + - pattern: | + $T = fmt.$P("...", ...) + ... + template.HTMLAttr($T, ...) + - pattern: | + $T, $ERR = fmt.$P("...", ...) + ... + template.HTMLAttr($T, ...) + - pattern: | + $T = $X + $Y + ... + template.HTMLAttr($T, ...) + - pattern: |- + $T = "..." + ... + $OTHER, $ERR = fmt.$P(..., $T, ...) + ... + template.HTMLAttr($OTHER, ...) diff --git a/go/lang/security/audit/net/unescaped-data-in-js.go b/go/lang/security/audit/net/unescaped-data-in-js.go new file mode 100644 index 00000000..70883c47 --- /dev/null +++ b/go/lang/security/audit/net/unescaped-data-in-js.go @@ -0,0 +1,15 @@ +package main + +import ( + "html/template" + "net/http" +) + +const tmpl = "" + +func Concat(r *http.Request) template.HTML { + customerId := r.URL.Query().Get("id") + // ruleid: unescaped-data-in-js + tmpl := "

" + customerId + "

" + return template.JS(tmpl) +} diff --git a/go/lang/security/audit/net/unescaped-data-in-js.yaml b/go/lang/security/audit/net/unescaped-data-in-js.yaml new file mode 100644 index 00000000..ff130a11 --- /dev/null +++ b/go/lang/security/audit/net/unescaped-data-in-js.yaml @@ -0,0 +1,53 @@ +rules: +- id: unescaped-data-in-js + message: >- + Found a formatted template string passed to 'template.JS()'. + 'template.JS()' does not escape contents. Be absolutely sure + there is no user-controlled data in this template. + metadata: + cwe: + - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')" + owasp: + - A07:2017 - Cross-Site Scripting (XSS) + - A03:2021 - Injection + references: + - https://golang.org/pkg/html/template/#JS + category: security + technology: + - go + confidence: LOW + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - audit + likelihood: LOW + impact: MEDIUM + languages: [go] + severity: WARNING + pattern-either: + - pattern: template.JS($T + $X, ...) + - pattern: template.JS(fmt.$P("...", ...), ...) + - pattern: | + $T = "..." + ... + $T = $FXN(..., $T, ...) + ... + template.JS($T, ...) + - pattern: | + $T = fmt.$P("...", ...) + ... + template.JS($T, ...) + - pattern: | + $T, $ERR = fmt.$P("...", ...) + ... + template.JS($T, ...) + - pattern: | + $T = $X + $Y + ... + template.JS($T, ...) + - pattern: | + $T = "..." + ... + $OTHER, $ERR = fmt.$P(..., $T, ...) + ... + template.JS($OTHER, ...) diff --git a/go/lang/security/audit/net/unescaped-data-in-url.go b/go/lang/security/audit/net/unescaped-data-in-url.go new file mode 100644 index 00000000..b6df704c --- /dev/null +++ b/go/lang/security/audit/net/unescaped-data-in-url.go @@ -0,0 +1,16 @@ +package main + +import ( + "html/template" + "net/http" +) + +const tmpl = "" + +func Concat(r *http.Request) template.HTML { + customerId := r.URL.Query().Get("id") + // ruleid: unescaped-data-in-url + tmpl := "

" + customerId + "

" + + return template.URL(tmpl) +} diff --git a/go/lang/security/audit/net/unescaped-data-in-url.yaml b/go/lang/security/audit/net/unescaped-data-in-url.yaml new file mode 100644 index 00000000..c88dd42b --- /dev/null +++ b/go/lang/security/audit/net/unescaped-data-in-url.yaml @@ -0,0 +1,54 @@ +rules: +- id: unescaped-data-in-url + message: >- + Found a formatted template string passed to 'template.URL()'. + 'template.URL()' does not escape contents, and this could result in XSS (cross-site scripting) and + therefore confidential data being stolen. Sanitize data coming into this function or make sure that no + user-controlled input is coming into the function. + metadata: + cwe: + - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')" + owasp: + - A07:2017 - Cross-Site Scripting (XSS) + - A03:2021 - Injection + references: + - https://golang.org/pkg/html/template/#URL + category: security + technology: + - go + confidence: LOW + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - audit + likelihood: LOW + impact: MEDIUM + languages: [go] + severity: WARNING + pattern-either: + - pattern: template.URL($T + $X, ...) + - pattern: template.URL(fmt.$P("...", ...), ...) + - pattern: | + $T = "..." + ... + $T = $FXN(..., $T, ...) + ... + template.URL($T, ...) + - pattern: | + $T = fmt.$P("...", ...) + ... + template.URL($T, ...) + - pattern: | + $T, $ERR = fmt.$P("...", ...) + ... + template.URL($T, ...) + - pattern: | + $T = $X + $Y + ... + template.URL($T, ...) + - pattern: |- + $T = "..." + ... + $OTHER, $ERR = fmt.$P(..., $T, ...) + ... + template.URL($OTHER, ...) diff --git a/go/lang/security/audit/net/use-tls.fixed.go b/go/lang/security/audit/net/use-tls.fixed.go new file mode 100644 index 00000000..20bc2da5 --- /dev/null +++ b/go/lang/security/audit/net/use-tls.fixed.go @@ -0,0 +1,17 @@ +package main + +import ( + "net/http" + "fmt" +) + +func Handler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.write([]byte("Hello, world!")) +} + +func main() { + http.HandleFunc("/index", Handler) + // ruleid: use-tls + http.ListenAndServeTLS(":80", certFile, keyFile, nil) +} diff --git a/go/lang/security/audit/net/use-tls.go b/go/lang/security/audit/net/use-tls.go new file mode 100644 index 00000000..62b89d43 --- /dev/null +++ b/go/lang/security/audit/net/use-tls.go @@ -0,0 +1,17 @@ +package main + +import ( + "net/http" + "fmt" +) + +func Handler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.write([]byte("Hello, world!")) +} + +func main() { + http.HandleFunc("/index", Handler) + // ruleid: use-tls + http.ListenAndServe(":80", nil) +} diff --git a/go/lang/security/audit/net/use-tls.yaml b/go/lang/security/audit/net/use-tls.yaml new file mode 100644 index 00000000..902b27c4 --- /dev/null +++ b/go/lang/security/audit/net/use-tls.yaml @@ -0,0 +1,25 @@ +rules: +- id: use-tls + pattern: http.ListenAndServe($ADDR, $HANDLER) + fix: http.ListenAndServeTLS($ADDR, certFile, keyFile, $HANDLER) + metadata: + cwe: + - 'CWE-319: Cleartext Transmission of Sensitive Information' + owasp: + - A03:2017 - Sensitive Data Exposure + - A02:2021 - Cryptographic Failures + references: + - https://golang.org/pkg/net/http/#ListenAndServeTLS + category: security + technology: + - go + confidence: MEDIUM + subcategory: + - audit + likelihood: LOW + impact: MEDIUM + message: >- + Found an HTTP server without TLS. Use 'http.ListenAndServeTLS' instead. + See https://golang.org/pkg/net/http/#ListenAndServeTLS for more information. + languages: [go] + severity: WARNING diff --git a/go/lang/security/audit/net/wip-xss-using-responsewriter-and-printf.go b/go/lang/security/audit/net/wip-xss-using-responsewriter-and-printf.go new file mode 100644 index 00000000..dd4636bb --- /dev/null +++ b/go/lang/security/audit/net/wip-xss-using-responsewriter-and-printf.go @@ -0,0 +1,65 @@ +package main + +import ( + "fmt" + "log" + "net/http" +) + +func getMovieQuote() map[string]string { + m := make(map[string]string) + m["quote"] = "I'll be back." + m["movie"] = "The Terminator" + m["year"] = "1984" + + return m +} + +func indexPage(w http.ResponseWriter, r *http.Request) { + const tme = `` + + const template = ` + + +

Random Movie Quotes

+

%s

+

~%s, %s

+ + ` + + quote := getMovieQuote() + + quoteText := quote["quote"] + movie := quote["movie"] + year := quote["year"] + + w.WriteHeader(http.StatusAccepted) + w.Write([]byte(fmt.Sprintf(template, quoteText, movie, year))) +} + +func errorPage(w http.ResponseWriter, r *http.Request) { + // ruleid: wip-xss-using-responsewriter-and-printf + params := r.URL.Query() + urls, ok := params["url"] + if !ok { + log.Println("Error") + return + } + url := urls[0] + + const template = ` + + +

error; page not found. go back

+ + ` + + w.WriteHeader(http.StatusAccepted) + w.Write([]byte(fmt.Sprintf(template, url))) +} + +func main() { + http.HandleFunc("/", indexPage) + http.HandleFunc("/error", errorPage) + http.ListenAndServe(":8080", nil) +} diff --git a/go/lang/security/audit/net/wip-xss-using-responsewriter-and-printf.yaml b/go/lang/security/audit/net/wip-xss-using-responsewriter-and-printf.yaml new file mode 100644 index 00000000..6d9b3b06 --- /dev/null +++ b/go/lang/security/audit/net/wip-xss-using-responsewriter-and-printf.yaml @@ -0,0 +1,72 @@ +rules: +- id: wip-xss-using-responsewriter-and-printf + patterns: + - pattern-inside: | + func $FUNC(..., $W http.ResponseWriter, ...) { + ... + var $TEMPLATE = "..." + ... + $W.Write([]byte(fmt.$PRINTF($TEMPLATE, ...)), ...) + ... + } + - pattern-either: + - pattern: | + $PARAMS = r.URL.Query() + ... + $DATA, $ERR := $PARAMS[...] + ... + $INTERM = $ANYTHING(..., $DATA, ...) + ... + $W.Write([]byte(fmt.$PRINTF(..., $INTERM, ...))) + - pattern: | + $PARAMS = r.URL.Query() + ... + $DATA, $ERR := $PARAMS[...] + ... + $INTERM = $DATA[...] + ... + $W.Write([]byte(fmt.$PRINTF(..., $INTERM, ...))) + - pattern: | + $DATA, $ERR := r.URL.Query()[...] + ... + $INTERM = $DATA[...] + ... + $W.Write([]byte(fmt.$PRINTF(..., $INTERM, ...))) + - pattern: | + $DATA, $ERR := r.URL.Query()[...] + ... + $INTERM = $ANYTHING(..., $DATA, ...) + ... + $W.Write([]byte(fmt.$PRINTF(..., $INTERM, ...))) + - pattern: | + $PARAMS = r.URL.Query() + ... + $DATA, $ERR := $PARAMS[...] + ... + $W.Write([]byte(fmt.$PRINTF(..., $DATA, ...))) + message: >- + Found data going from url query parameters into formatted data written to ResponseWriter. + This could be XSS and should not be done. If you must do this, ensure your data + is + sanitized or escaped. + metadata: + cwe: + - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')" + owasp: + - A07:2017 - Cross-Site Scripting (XSS) + - A03:2021 - Injection + category: security + technology: + - go + confidence: MEDIUM + references: + - https://owasp.org/Top10/A03_2021-Injection + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - vuln + likelihood: LOW + impact: MEDIUM + severity: WARNING + languages: + - go diff --git a/go/lang/security/audit/reflect-makefunc.go b/go/lang/security/audit/reflect-makefunc.go new file mode 100644 index 00000000..6b73c2fa --- /dev/null +++ b/go/lang/security/audit/reflect-makefunc.go @@ -0,0 +1,831 @@ +/* +* Test case reference: +* https://github.com/robertkrimen/otto//blob/c382bd3c16ff2fef9b5fe0dd8bf4c4ec6bfe62c1/runtime.go#L489 + */ + +package main + +import ( + "encoding" + "encoding/json" + "errors" + "fmt" + "math" + "path" + "reflect" + "runtime" + "strconv" + "strings" + "sync" + + "github.com/robertkrimen/otto/ast" + "github.com/robertkrimen/otto/parser" +) + +type _global struct { + Object *_object // Object( ... ), new Object( ... ) - 1 (length) + Function *_object // Function( ... ), new Function( ... ) - 1 + Array *_object // Array( ... ), new Array( ... ) - 1 + String *_object // String( ... ), new String( ... ) - 1 + Boolean *_object // Boolean( ... ), new Boolean( ... ) - 1 + Number *_object // Number( ... ), new Number( ... ) - 1 + Math *_object + Date *_object // Date( ... ), new Date( ... ) - 7 + RegExp *_object // RegExp( ... ), new RegExp( ... ) - 2 + Error *_object // Error( ... ), new Error( ... ) - 1 + EvalError *_object + TypeError *_object + RangeError *_object + ReferenceError *_object + SyntaxError *_object + URIError *_object + JSON *_object + + ObjectPrototype *_object // Object.prototype + FunctionPrototype *_object // Function.prototype + ArrayPrototype *_object // Array.prototype + StringPrototype *_object // String.prototype + BooleanPrototype *_object // Boolean.prototype + NumberPrototype *_object // Number.prototype + DatePrototype *_object // Date.prototype + RegExpPrototype *_object // RegExp.prototype + ErrorPrototype *_object // Error.prototype + EvalErrorPrototype *_object + TypeErrorPrototype *_object + RangeErrorPrototype *_object + ReferenceErrorPrototype *_object + SyntaxErrorPrototype *_object + URIErrorPrototype *_object +} + +type _runtime struct { + global _global + globalObject *_object + globalStash *_objectStash + scope *_scope + otto *Otto + eval *_object // The builtin eval, for determine indirect versus direct invocation + debugger func(*Otto) + random func() float64 + stackLimit int + traceLimit int + + labels []string // FIXME + lck sync.Mutex +} + +func (self *_runtime) enterScope(scope *_scope) { + scope.outer = self.scope + if self.scope != nil { + if self.stackLimit != 0 && self.scope.depth+1 >= self.stackLimit { + panic(self.panicRangeError("Maximum call stack size exceeded")) + } + + scope.depth = self.scope.depth + 1 + } + + self.scope = scope +} + +func (self *_runtime) leaveScope() { + self.scope = self.scope.outer +} + +// FIXME This is used in two places (cloning) +func (self *_runtime) enterGlobalScope() { + self.enterScope(newScope(self.globalStash, self.globalStash, self.globalObject)) +} + +func (self *_runtime) enterFunctionScope(outer _stash, this Value) *_fnStash { + if outer == nil { + outer = self.globalStash + } + stash := self.newFunctionStash(outer) + var thisObject *_object + switch this.kind { + case valueUndefined, valueNull: + thisObject = self.globalObject + default: + thisObject = self.toObject(this) + } + self.enterScope(newScope(stash, stash, thisObject)) + return stash +} + +func (self *_runtime) putValue(reference _reference, value Value) { + name := reference.putValue(value) + if name != "" { + // Why? -- If reference.base == nil + // strict = false + self.globalObject.defineProperty(name, value, 0111, false) + } +} + +func (self *_runtime) tryCatchEvaluate(inner func() Value) (tryValue Value, exception bool) { + // resultValue = The value of the block (e.g. the last statement) + // throw = Something was thrown + // throwValue = The value of what was thrown + // other = Something that changes flow (return, break, continue) that is not a throw + // Otherwise, some sort of unknown panic happened, we'll just propagate it + defer func() { + if caught := recover(); caught != nil { + if exception, ok := caught.(*_exception); ok { + caught = exception.eject() + } + switch caught := caught.(type) { + case _error: + exception = true + tryValue = toValue_object(self.newError(caught.name, caught.messageValue(), 0)) + case Value: + exception = true + tryValue = caught + default: + panic(caught) + } + } + }() + + tryValue = inner() + return +} + +// toObject + +func (self *_runtime) toObject(value Value) *_object { + switch value.kind { + case valueEmpty, valueUndefined, valueNull: + panic(self.panicTypeError()) + case valueBoolean: + return self.newBoolean(value) + case valueString: + return self.newString(value) + case valueNumber: + return self.newNumber(value) + case valueObject: + return value._object() + } + panic(self.panicTypeError()) +} + +func (self *_runtime) objectCoerce(value Value) (*_object, error) { + switch value.kind { + case valueUndefined: + return nil, errors.New("undefined") + case valueNull: + return nil, errors.New("null") + case valueBoolean: + return self.newBoolean(value), nil + case valueString: + return self.newString(value), nil + case valueNumber: + return self.newNumber(value), nil + case valueObject: + return value._object(), nil + } + panic(self.panicTypeError()) +} + +func checkObjectCoercible(rt *_runtime, value Value) { + isObject, mustCoerce := testObjectCoercible(value) + if !isObject && !mustCoerce { + panic(rt.panicTypeError()) + } +} + +// testObjectCoercible + +func testObjectCoercible(value Value) (isObject bool, mustCoerce bool) { + switch value.kind { + case valueReference, valueEmpty, valueNull, valueUndefined: + return false, false + case valueNumber, valueString, valueBoolean: + return false, true + case valueObject: + return true, false + default: + panic("this should never happen") + } +} + +func (self *_runtime) safeToValue(value interface{}) (Value, error) { + result := Value{} + err := catchPanic(func() { + result = self.toValue(value) + }) + return result, err +} + +// convertNumeric converts numeric parameter val from js to that of type t if it is safe to do so, otherwise it panics. +// This allows literals (int64), bitwise values (int32) and the general form (float64) of javascript numerics to be passed as parameters to go functions easily. +func (self *_runtime) convertNumeric(v Value, t reflect.Type) reflect.Value { + val := reflect.ValueOf(v.export()) + + if val.Kind() == t.Kind() { + return val + } + + if val.Kind() == reflect.Interface { + val = reflect.ValueOf(val.Interface()) + } + + switch val.Kind() { + case reflect.Float32, reflect.Float64: + f64 := val.Float() + switch t.Kind() { + case reflect.Float64: + return reflect.ValueOf(f64) + case reflect.Float32: + if reflect.Zero(t).OverflowFloat(f64) { + panic(self.panicRangeError("converting float64 to float32 would overflow")) + } + + return val.Convert(t) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + i64 := int64(f64) + if float64(i64) != f64 { + panic(self.panicRangeError(fmt.Sprintf("converting %v to %v would cause loss of precision", val.Type(), t))) + } + + // The float represents an integer + val = reflect.ValueOf(i64) + default: + panic(self.panicTypeError(fmt.Sprintf("cannot convert %v to %v", val.Type(), t))) + } + } + + switch val.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + i64 := val.Int() + switch t.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if reflect.Zero(t).OverflowInt(i64) { + panic(self.panicRangeError(fmt.Sprintf("converting %v to %v would overflow", val.Type(), t))) + } + return val.Convert(t) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if i64 < 0 { + panic(self.panicRangeError(fmt.Sprintf("converting %v to %v would underflow", val.Type(), t))) + } + if reflect.Zero(t).OverflowUint(uint64(i64)) { + panic(self.panicRangeError(fmt.Sprintf("converting %v to %v would overflow", val.Type(), t))) + } + return val.Convert(t) + case reflect.Float32, reflect.Float64: + return val.Convert(t) + } + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + u64 := val.Uint() + switch t.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if u64 > math.MaxInt64 || reflect.Zero(t).OverflowInt(int64(u64)) { + panic(self.panicRangeError(fmt.Sprintf("converting %v to %v would overflow", val.Type(), t))) + } + return val.Convert(t) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if reflect.Zero(t).OverflowUint(u64) { + panic(self.panicRangeError(fmt.Sprintf("converting %v to %v would overflow", val.Type(), t))) + } + return val.Convert(t) + case reflect.Float32, reflect.Float64: + return val.Convert(t) + } + } + + panic(self.panicTypeError(fmt.Sprintf("unsupported type %v -> %v for numeric conversion", val.Type(), t))) +} + +func fieldIndexByName(t reflect.Type, name string) []int { + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + + if !validGoStructName(f.Name) { + continue + } + + if f.Anonymous { + if a := fieldIndexByName(f.Type, name); a != nil { + return append([]int{i}, a...) + } + } + + if a := strings.SplitN(f.Tag.Get("json"), ",", 2); a[0] != "" { + if a[0] == "-" { + continue + } + + if a[0] == name { + return []int{i} + } + } + + if f.Name == name { + return []int{i} + } + } + + return nil +} + +var typeOfValue = reflect.TypeOf(Value{}) +var typeOfJSONRawMessage = reflect.TypeOf(json.RawMessage{}) + +// convertCallParameter converts request val to type t if possible. +// If the conversion fails due to overflow or type miss-match then it panics. +// If no conversion is known then the original value is returned. +func (self *_runtime) convertCallParameter(v Value, t reflect.Type) reflect.Value { + if t == typeOfValue { + return reflect.ValueOf(v) + } + + if t == typeOfJSONRawMessage { + if d, err := json.Marshal(v.export()); err == nil { + return reflect.ValueOf(d) + } + } + + if v.kind == valueObject { + if gso, ok := v._object().value.(*_goStructObject); ok { + if gso.value.Type().AssignableTo(t) { + // please see TestDynamicFunctionReturningInterface for why this exists + if t.Kind() == reflect.Interface && gso.value.Type().ConvertibleTo(t) { + return gso.value.Convert(t) + } else { + return gso.value + } + } + } + + if gao, ok := v._object().value.(*_goArrayObject); ok { + if gao.value.Type().AssignableTo(t) { + // please see TestDynamicFunctionReturningInterface for why this exists + if t.Kind() == reflect.Interface && gao.value.Type().ConvertibleTo(t) { + return gao.value.Convert(t) + } else { + return gao.value + } + } + } + } + + if t.Kind() == reflect.Interface { + e := v.export() + if e == nil { + return reflect.Zero(t) + } + iv := reflect.ValueOf(e) + if iv.Type().AssignableTo(t) { + return iv + } + } + + tk := t.Kind() + + if tk == reflect.Ptr { + switch v.kind { + case valueEmpty, valueNull, valueUndefined: + return reflect.Zero(t) + default: + var vv reflect.Value + if err := catchPanic(func() { vv = self.convertCallParameter(v, t.Elem()) }); err == nil { + if vv.CanAddr() { + return vv.Addr() + } + + pv := reflect.New(vv.Type()) + pv.Elem().Set(vv) + return pv + } + } + } + + switch tk { + case reflect.Bool: + return reflect.ValueOf(v.bool()) + case reflect.String: + switch v.kind { + case valueString: + return reflect.ValueOf(v.value) + case valueNumber: + return reflect.ValueOf(fmt.Sprintf("%v", v.value)) + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64: + switch v.kind { + case valueNumber: + return self.convertNumeric(v, t) + } + case reflect.Slice: + if o := v._object(); o != nil { + if lv := o.get("length"); lv.IsNumber() { + l := lv.number().int64 + + s := reflect.MakeSlice(t, int(l), int(l)) + + tt := t.Elem() + + if o.class == "Array" { + for i := int64(0); i < l; i++ { + p, ok := o.property[strconv.FormatInt(i, 10)] + if !ok { + continue + } + + e, ok := p.value.(Value) + if !ok { + continue + } + + ev := self.convertCallParameter(e, tt) + + s.Index(int(i)).Set(ev) + } + } else if o.class == "GoArray" { + + var gslice bool + switch o.value.(type) { + case *_goSliceObject: + gslice = true + case *_goArrayObject: + gslice = false + } + + for i := int64(0); i < l; i++ { + var p *_property + if gslice { + p = goSliceGetOwnProperty(o, strconv.FormatInt(i, 10)) + } else { + p = goArrayGetOwnProperty(o, strconv.FormatInt(i, 10)) + } + if p == nil { + continue + } + + e, ok := p.value.(Value) + if !ok { + continue + } + + ev := self.convertCallParameter(e, tt) + + s.Index(int(i)).Set(ev) + } + } + + return s + } + } + case reflect.Map: + if o := v._object(); o != nil && t.Key().Kind() == reflect.String { + m := reflect.MakeMap(t) + + o.enumerate(false, func(k string) bool { + m.SetMapIndex(reflect.ValueOf(k), self.convertCallParameter(o.get(k), t.Elem())) + return true + }) + + return m + } + case reflect.Func: + if t.NumOut() > 1 { + panic(self.panicTypeError("converting JavaScript values to Go functions with more than one return value is currently not supported")) + } + + if o := v._object(); o != nil && o.class == "Function" { + // ruleid: reflect-makefunc + return reflect.MakeFunc(t, func(args []reflect.Value) []reflect.Value { + l := make([]interface{}, len(args)) + for i, a := range args { + if a.CanInterface() { + l[i] = a.Interface() + } + } + + rv, err := v.Call(nullValue, l...) + if err != nil { + panic(err) + } + + if t.NumOut() == 0 { + return nil + } + + return []reflect.Value{self.convertCallParameter(rv, t.Out(0))} + }) + } + case reflect.Struct: + if o := v._object(); o != nil && o.class == "Object" { + s := reflect.New(t) + + for _, k := range o.propertyOrder { + idx := fieldIndexByName(t, k) + + if idx == nil { + panic(self.panicTypeError("can't convert object; field %q was supplied but does not exist on target %v", k, t)) + } + + ss := s + + for _, i := range idx { + if ss.Kind() == reflect.Ptr { + if ss.IsNil() { + if !ss.CanSet() { + panic(self.panicTypeError("can't set embedded pointer to unexported struct: %v", ss.Type().Elem())) + } + + ss.Set(reflect.New(ss.Type().Elem())) + } + + ss = ss.Elem() + } + + ss = ss.Field(i) + } + + ss.Set(self.convertCallParameter(o.get(k), ss.Type())) + } + + return s.Elem() + } + } + + if tk == reflect.String { + if o := v._object(); o != nil && o.hasProperty("toString") { + if fn := o.get("toString"); fn.IsFunction() { + sv, err := fn.Call(v) + if err != nil { + panic(err) + } + + var r reflect.Value + if err := catchPanic(func() { r = self.convertCallParameter(sv, t) }); err == nil { + return r + } + } + } + + return reflect.ValueOf(v.String()) + } + + if v.kind == valueString { + var s encoding.TextUnmarshaler + + if reflect.PtrTo(t).Implements(reflect.TypeOf(&s).Elem()) { + r := reflect.New(t) + + if err := r.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(v.string())); err != nil { + panic(self.panicSyntaxError("can't convert to %s: %s", t.String(), err.Error())) + } + + return r.Elem() + } + } + + s := "OTTO DOES NOT UNDERSTAND THIS TYPE" + switch v.kind { + case valueBoolean: + s = "boolean" + case valueNull: + s = "null" + case valueNumber: + s = "number" + case valueString: + s = "string" + case valueUndefined: + s = "undefined" + case valueObject: + s = v.Class() + } + + panic(self.panicTypeError("can't convert from %q to %q", s, t)) +} + +func (self *_runtime) toValue(value interface{}) Value { + switch value := value.(type) { + case Value: + return value + case func(FunctionCall) Value: + var name, file string + var line int + pc := reflect.ValueOf(value).Pointer() + fn := runtime.FuncForPC(pc) + if fn != nil { + name = fn.Name() + file, line = fn.FileLine(pc) + file = path.Base(file) + } + return toValue_object(self.newNativeFunction(name, file, line, value)) + case _nativeFunction: + var name, file string + var line int + pc := reflect.ValueOf(value).Pointer() + fn := runtime.FuncForPC(pc) + if fn != nil { + name = fn.Name() + file, line = fn.FileLine(pc) + file = path.Base(file) + } + return toValue_object(self.newNativeFunction(name, file, line, value)) + case Object, *Object, _object, *_object: + // Nothing happens. + // FIXME We should really figure out what can come here. + // This catch-all is ugly. + default: + { + value := reflect.ValueOf(value) + + switch value.Kind() { + case reflect.Ptr: + switch reflect.Indirect(value).Kind() { + case reflect.Struct: + return toValue_object(self.newGoStructObject(value)) + case reflect.Array: + return toValue_object(self.newGoArray(value)) + } + case reflect.Struct: + return toValue_object(self.newGoStructObject(value)) + case reflect.Map: + return toValue_object(self.newGoMapObject(value)) + case reflect.Slice: + return toValue_object(self.newGoSlice(value)) + case reflect.Array: + return toValue_object(self.newGoArray(value)) + case reflect.Func: + var name, file string + var line int + if v := reflect.ValueOf(value); v.Kind() == reflect.Ptr { + pc := v.Pointer() + fn := runtime.FuncForPC(pc) + if fn != nil { + name = fn.Name() + file, line = fn.FileLine(pc) + file = path.Base(file) + } + } + + typ := value.Type() + + return toValue_object(self.newNativeFunction(name, file, line, func(c FunctionCall) Value { + nargs := typ.NumIn() + + if len(c.ArgumentList) != nargs { + if typ.IsVariadic() { + if len(c.ArgumentList) < nargs-1 { + panic(self.panicRangeError(fmt.Sprintf("expected at least %d arguments; got %d", nargs-1, len(c.ArgumentList)))) + } + } else { + panic(self.panicRangeError(fmt.Sprintf("expected %d argument(s); got %d", nargs, len(c.ArgumentList)))) + } + } + + in := make([]reflect.Value, len(c.ArgumentList)) + + callSlice := false + + for i, a := range c.ArgumentList { + var t reflect.Type + + n := i + if n >= nargs-1 && typ.IsVariadic() { + if n > nargs-1 { + n = nargs - 1 + } + + t = typ.In(n).Elem() + } else { + t = typ.In(n) + } + + // if this is a variadic Go function, and the caller has supplied + // exactly the number of JavaScript arguments required, and this + // is the last JavaScript argument, try treating the it as the + // actual set of variadic Go arguments. if that succeeds, break + // out of the loop. + if typ.IsVariadic() && len(c.ArgumentList) == nargs && i == nargs-1 { + var v reflect.Value + if err := catchPanic(func() { v = self.convertCallParameter(a, typ.In(n)) }); err == nil { + in[i] = v + callSlice = true + break + } + } + + in[i] = self.convertCallParameter(a, t) + } + + var out []reflect.Value + if callSlice { + out = value.CallSlice(in) + } else { + out = value.Call(in) + } + + switch len(out) { + case 0: + return Value{} + case 1: + return self.toValue(out[0].Interface()) + default: + s := make([]interface{}, len(out)) + for i, v := range out { + s[i] = self.toValue(v.Interface()) + } + + return self.toValue(s) + } + })) + } + } + } + + return toValue(value) +} + +func (runtime *_runtime) newGoSlice(value reflect.Value) *_object { + self := runtime.newGoSliceObject(value) + self.prototype = runtime.global.ArrayPrototype + return self +} + +func (runtime *_runtime) newGoArray(value reflect.Value) *_object { + self := runtime.newGoArrayObject(value) + self.prototype = runtime.global.ArrayPrototype + return self +} + +func (runtime *_runtime) parse(filename string, src, sm interface{}) (*ast.Program, error) { + return parser.ParseFileWithSourceMap(nil, filename, src, sm, 0) +} + +func (runtime *_runtime) cmpl_parse(filename string, src, sm interface{}) (*_nodeProgram, error) { + program, err := parser.ParseFileWithSourceMap(nil, filename, src, sm, 0) + if err != nil { + return nil, err + } + + return cmpl_parse(program), nil +} + +func (self *_runtime) parseSource(src, sm interface{}) (*_nodeProgram, *ast.Program, error) { + switch src := src.(type) { + case *ast.Program: + return nil, src, nil + case *Script: + return src.program, nil, nil + } + + program, err := self.parse("", src, sm) + + return nil, program, err +} + +func (self *_runtime) cmpl_runOrEval(src, sm interface{}, eval bool) (Value, error) { + result := Value{} + cmpl_program, program, err := self.parseSource(src, sm) + if err != nil { + return result, err + } + if cmpl_program == nil { + cmpl_program = cmpl_parse(program) + } + err = catchPanic(func() { + result = self.cmpl_evaluate_nodeProgram(cmpl_program, eval) + }) + switch result.kind { + case valueEmpty: + result = Value{} + case valueReference: + result = result.resolve() + } + return result, err +} + +func (self *_runtime) cmpl_run(src, sm interface{}) (Value, error) { + return self.cmpl_runOrEval(src, sm, false) +} + +func (self *_runtime) cmpl_eval(src, sm interface{}) (Value, error) { + return self.cmpl_runOrEval(src, sm, true) +} + +func (self *_runtime) parseThrow(err error) { + if err == nil { + return + } + switch err := err.(type) { + case parser.ErrorList: + { + err := err[0] + if err.Message == "Invalid left-hand side in assignment" { + panic(self.panicReferenceError(err.Message)) + } + panic(self.panicSyntaxError(err.Message)) + } + } + panic(self.panicSyntaxError(err.Error())) +} + +func (self *_runtime) cmpl_parseOrThrow(src, sm interface{}) *_nodeProgram { + program, err := self.cmpl_parse("", src, sm) + self.parseThrow(err) // Will panic/throw appropriately + return program +} diff --git a/go/lang/security/audit/reflect-makefunc.yaml b/go/lang/security/audit/reflect-makefunc.yaml new file mode 100644 index 00000000..0200a485 --- /dev/null +++ b/go/lang/security/audit/reflect-makefunc.yaml @@ -0,0 +1,26 @@ +rules: +- id: reflect-makefunc + message: >- + 'reflect.MakeFunc' detected. This will sidestep protections that are + normally afforded by Go's type system. Audit this call and be sure that + user input cannot be used to affect the code generated by MakeFunc; + otherwise, you will have a serious security vulnerability. + metadata: + owasp: + - A01:2021 - Broken Access Control + cwe: + - 'CWE-913: Improper Control of Dynamically-Managed Code Resources' + category: security + technology: + - go + confidence: LOW + references: + - https://owasp.org/Top10/A01_2021-Broken_Access_Control + subcategory: + - audit + likelihood: LOW + impact: LOW + severity: ERROR + pattern: reflect.MakeFunc(...) + languages: + - go diff --git a/go/lang/security/audit/sqli/gosql-sqli.go b/go/lang/security/audit/sqli/gosql-sqli.go new file mode 100644 index 00000000..57eb97d5 --- /dev/null +++ b/go/lang/security/audit/sqli/gosql-sqli.go @@ -0,0 +1,72 @@ +package main + +import "database/sql" +import "fmt" + +func bad1() { + db, err := sql.Open("mysql", "theUser:thePassword@/theDbName") + if err != nil { + panic(err) + } + query = "SELECT name FROM users WHERE age=" + req.FormValue("age") + // ruleid: gosql-sqli + db.Query(query) +} + +func bad2(db *sql.DB) { + query = "SELECT name FROM users WHERE age=" + query += req.FormValue("age") + // ruleid: gosql-sqli + db.QueryRow(query) +} + +func bad3(db *sql.DB) { + query = fmt.Sprintf("SELECT * FROM users WHERE email='%s';", email) + // ruleid: gosql-sqli + db.Exec(query) +} + +func bad4(db *sql.DB) { + // ruleid: gosql-sqli + db.Exec("SELECT name FROM users WHERE age=" + req.FormValue("age")) +} + +func bad5(db *sql.DB) { + // ruleid: gosql-sqli + db.Exec(fmt.Sprintf("SELECT * FROM users WHERE email='%s';", email)) +} + +func ok1(db *sql.DB) { + query = fmt.Sprintf("SELECT * FROM users WHERE email=hello;") + // ok: gosql-sqli + db.Exec(query) +} + +func ok2(db *sql.DB) { + query = "SELECT name FROM users WHERE age=" + "3" + // ok: gosql-sqli + db.Query(query) +} + +func ok3(db *sql.DB) { + query = "SELECT name FROM users WHERE age=" + query += "3" + // ok: gosql-sqli + db.Query(query) +} + +func ok4(db *sql.DB) { + // ok: gosql-sqli + db.Exec("INSERT INTO users(name, email) VALUES($1, $2)", + "Jon Calhoun", "jon@calhoun.io") +} + +func ok5(db *sql.DB) { + // ok: gosql-sqli + db.Exec("SELECT name FROM users WHERE age=" + "3") +} + +func ok6(db *sql.DB) { + // ok: gosql-sqli + db.Exec(fmt.Sprintf("SELECT * FROM users WHERE email=hello;")) +} diff --git a/go/lang/security/audit/sqli/gosql-sqli.yaml b/go/lang/security/audit/sqli/gosql-sqli.yaml new file mode 100644 index 00000000..fee58cbe --- /dev/null +++ b/go/lang/security/audit/sqli/gosql-sqli.yaml @@ -0,0 +1,63 @@ +rules: +- id: gosql-sqli + patterns: + - pattern-either: + - patterns: + - pattern: $DB.$METHOD(...,$QUERY,...) + - pattern-either: + - pattern-inside: | + $QUERY = $X + $Y + ... + - pattern-inside: | + $QUERY += $X + ... + - pattern-inside: | + $QUERY = fmt.Sprintf("...", $PARAM1, ...) + ... + - pattern-not-inside: | + $QUERY += "..." + ... + - pattern-not-inside: | + $QUERY = "..." + "..." + ... + - pattern: $DB.$METHOD(..., $X + $Y, ...) + - pattern: $DB.$METHOD(..., fmt.Sprintf("...", $PARAM1, ...), ...) + - pattern-either: + - pattern-inside: | + $DB, ... = sql.Open(...) + ... + - pattern-inside: | + func $FUNCNAME(..., $DB *sql.DB, ...) { + ... + } + - pattern-not: $DB.$METHOD(..., "..." + "...", ...) + - metavariable-regex: + metavariable: $METHOD + regex: ^(Exec|ExecContent|Query|QueryContext|QueryRow|QueryRowContext)$ + languages: + - go + message: >- + Detected string concatenation with a non-literal variable in a "database/sql" + Go SQL statement. This could lead to SQL injection if the variable is user-controlled + and not properly sanitized. In order to prevent SQL injection, + use parameterized queries or prepared statements instead. + You can use prepared statements with the 'Prepare' and 'PrepareContext' calls. + metadata: + cwe: + - "CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')" + references: + - https://golang.org/pkg/database/sql/ + category: security + technology: + - go + confidence: LOW + owasp: + - A01:2017 - Injection + - A03:2021 - Injection + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - vuln + likelihood: LOW + impact: HIGH + severity: ERROR diff --git a/go/lang/security/audit/sqli/pg-orm-sqli.go b/go/lang/security/audit/sqli/pg-orm-sqli.go new file mode 100644 index 00000000..8940dd1f --- /dev/null +++ b/go/lang/security/audit/sqli/pg-orm-sqli.go @@ -0,0 +1,149 @@ +package main + +import ( + "fmt" + "path" + + "github.com/go-pg/pg/v10" + "github.com/go-pg/pg/v10/orm" +) + +func bad1() { + db := pg.Connect(&pg.Options{ + Addr: ":5432", + User: "user", + Password: "pass", + Database: "db_name", + }) + query = "SELECT name FROM users WHERE age=" + req.FormValue("age") + // ruleid: pg-orm-sqli + err := db.Model(book). + Where("id > ?", 100). + WhereOr(query). + Limit(1). + Select() +} + +func bad2() { + db := pg.Connect(opt) + query = fmt.Sprintf("SELECT * FROM users WHERE email='%s';", email) + story := new(Story) + // ruleid: pg-orm-sqli + err = db.Model(story). + Relation("Author"). + From("Hello"). + Where("SELECT name FROM users WHERE age=" + req.FormValue("age")). + Select() + if err != nil { + panic(err) + } +} + +func bad3() { + opt, err := pg.ParseURL("postgres://user:pass@localhost:5432/db_name") + if err != nil { + panic(err) + } + + db := pg.Connect(opt) + + query = "SELECT name FROM users WHERE age=" + query += req.FormValue("age") + // ruleid: pg-orm-sqli + err := db.Model(book). + Where(query). + WhereGroup(func(q *pg.Query) (*pg.Query, error) { + q = q.WhereOr("id = 1"). + WhereOr("id = 2") + return q, nil + }). + Limit(1). + Select() +} + +func bad4(db *pg.DB) { + query = fmt.Sprintf("SELECT * FROM users WHERE email='%s';", email) + // ruleid: pg-orm-sqli + err := db.Model((*Book)(nil)). + Column("author_id"). + ColumnExpr(query). + Group("author_id"). + Order("book_count DESC"). + Select(&res) +} + +func bad5(db *pg.DB) { + // ruleid: pg-orm-sqli + err = db.Model((*Book)(nil)). + Column("title", "text"). + Where("SELECT name FROM users WHERE age=" + req.FormValue("age")). + Select() +} + +func bad6(db *pg.DB) { + // ruleid: pg-orm-sqli + err = db.Model((*Book)(nil)). + Column("title", "text"). + Where(fmt.Sprintf("SELECT * FROM users WHERE email='%s';", email)). + Select() +} + +func ok1(db *pg.DB) { + query = fmt.Sprintf("SELECT * FROM users WHERE email=hello;") + // ok: pg-orm-sqli + err = db.Model((*Book)(nil)). + Column("title", "text"). + Where(query). + Select() +} + +func ok2(db *pg.DB) { + query = "SELECT name FROM users WHERE age=" + "3" + // ok: pg-orm-sqli + err = db.Model((*Book)(nil)). + Column("title", "text"). + ColumnExpr(query). + Select() +} + +func ok3(db *pg.DB) { + query = "SELECT name FROM users WHERE age=" + query += "3" + // ok: pg-orm-sqli + err = db.Model((*Book)(nil)). + Column("title", "text"). + Where(query). + Select() +} + +func ok4(db *pg.DB) { + // ok: pg-orm-sqli + err := db.Model((*Book)(nil)). + Column("title", "text"). + Where("id = ?", 1). + Select(&title, &text) +} + +func ok5(db *pg.DB) { + // ok: pg-orm-sqli + err := db.Model((*Book)(nil)). + Column("title", "text"). + Where("SELECT name FROM users WHERE age=" + "3"). + Select(&title, &text) +} + +func ok6(db *pg.DB) { + // ok: pg-orm-sqli + err := db.Model(). + ColumnExpr(fmt.Sprintf("SELECT * FROM users WHERE email=hello;")) +} + +func ok7() { + // ok: pg-orm-sqli + path.Join("foo", fmt.Sprintf("%s.baz", "bar")) +} + +func ok8() { + // ok: pg-orm-sqli + filepath.Join("foo", fmt.Sprintf("%s.baz", "bar")) +} diff --git a/go/lang/security/audit/sqli/pg-orm-sqli.yaml b/go/lang/security/audit/sqli/pg-orm-sqli.yaml new file mode 100644 index 00000000..04a4da6b --- /dev/null +++ b/go/lang/security/audit/sqli/pg-orm-sqli.yaml @@ -0,0 +1,87 @@ +rules: + - id: pg-orm-sqli + patterns: + - pattern-inside: | + import ( + ... + "$IMPORT" + ) + ... + - metavariable-regex: + metavariable: $IMPORT + regex: .*go-pg + - pattern-either: + - patterns: + - pattern: $DB.$METHOD(...,$QUERY,...) + - pattern-either: + - pattern-inside: | + $QUERY = $X + $Y + ... + - pattern-inside: | + $QUERY += $X + ... + - pattern-inside: | + $QUERY = fmt.Sprintf("...", $PARAM1, ...) + ... + - pattern-not-inside: | + $QUERY += "..." + ... + - pattern-not-inside: | + $QUERY = "..." + "..." + ... + - pattern: | + $DB.$INTFUNC1(...).$METHOD(..., $X + $Y, ...).$INTFUNC2(...) + - pattern: | + $DB.$METHOD(..., fmt.Sprintf("...", $PARAM1, ...), ...) + - pattern-inside: | + $DB = pg.Connect(...) + ... + - pattern-inside: | + func $FUNCNAME(..., $DB *pg.DB, ...) { + ... + } + - pattern-not-inside: | + $QUERY = fmt.Sprintf("...", ...,"...", ...) + ... + - pattern-not-inside: | + $QUERY += "..." + ... + - pattern-not: $DB.$METHOD(...,"...",...) + - pattern-not: | + $DB.$INTFUNC1(...).$METHOD(..., "...", ...).$INTFUNC2(...) + - pattern-not-inside: | + $QUERY = "..." + "..." + - pattern-not: | + "..." + - pattern-not: path.Join(...) + - pattern-not: filepath.Join(...) + - metavariable-regex: + metavariable: $METHOD + regex: ^(Where|WhereOr|Join|GroupExpr|OrderExpr|ColumnExpr)$ + languages: + - go + message: Detected string concatenation with a non-literal variable in a go-pg + ORM SQL statement. This could lead to SQL injection if the variable is + user-controlled and not properly sanitized. In order to prevent SQL + injection, do not use strings concatenated with user-controlled input. + Instead, use parameterized statements. + metadata: + cwe: + - "CWE-89: Improper Neutralization of Special Elements used in an SQL + Command ('SQL Injection')" + references: + - https://pg.uptrace.dev/queries/ + category: security + technology: + - go-pg + confidence: LOW + owasp: + - A01:2017 - Injection + - A03:2021 - Injection + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - vuln + likelihood: LOW + impact: HIGH + severity: ERROR diff --git a/go/lang/security/audit/sqli/pg-sqli.go b/go/lang/security/audit/sqli/pg-sqli.go new file mode 100644 index 00000000..bfa8e0c5 --- /dev/null +++ b/go/lang/security/audit/sqli/pg-sqli.go @@ -0,0 +1,111 @@ +package main + +import ( + "fmt" + + "github.com/go-pg/pg/v10" + "github.com/go-pg/pg/v10/orm" +) + +func bad1() { + db := pg.Connect(&pg.Options{ + Addr: ":5432", + User: "user", + Password: "pass", + Database: "db_name", + }) + query = "SELECT name FROM users WHERE age=" + req.FormValue("age") + // ruleid: pg-sqli + rows, err := db.ExecContext(query) +} + +func bad2() { + opt, err := pg.ParseURL("postgres://user:pass@localhost:5432/db_name") + if err != nil { + panic(err) + } + + db := pg.Connect(opt) + + query = "SELECT name FROM users WHERE age=" + req.FormValue("age") + // ruleid: pg-sqli + rows, err := db.Exec(ctx, query) +} + +func bad3() { + opt, err := pg.ParseURL("postgres://user:pass@localhost:5432/ db_name") + if err != nil { + panic(err) + } + + db := pg.Connect(opt) + query = "SELECT name FROM users WHERE age=" + query += req.FormValue("age") + // ruleid: pg-sqli + db.QueryContext(ctx, query) +} + +func bad4(db *pg.DB) { + query = fmt.Sprintf("SELECT * FROM users WHERE email='%s';", email) + // ruleid: pg-sqli + db.Query(ctx, query) +} + +func bad5(db *pg.DB) { + // ruleid: pg-sqli + db.Exec(ctx, "SELECT name FROM users WHERE age=" + req.FormValue("age")) +} + +func bad6(db *pg.DB) { + // ruleid: pg-sqli + db.QueryOne(ctx, fmt.Sprintf("SELECT * FROM users WHERE email='%s';", email)) +} + +func ok1(db *pg.DB) { + query = fmt.Sprintf("SELECT * FROM users WHERE email=hello;") + // ok: pg-sqli + db.QueryContext(ctx, query) +} + +func ok2(db *pg.DB) { + query = "SELECT name FROM users WHERE age=" + "3" + // ok: pg-sqli + db.Query(ctx, query) +} + +func ok3(db *pg.DB) { + query = "SELECT name FROM users WHERE age=" + query += "3" + // ok: pg-sqli + db.QueryRowContext(ctx, query) +} + +func ok4(db *pg.DB) { + // ok: pg-sqli + db.Exec(ctx, "INSERT INTO users(name, email) VALUES($1, $2)", + "Jon Calhoun", "jon@calhoun.io") +} + +func ok5(db *pg.DB) { + // ok: pg-sqli + db.Exec("SELECT name FROM users WHERE age=" + "3") +} + +func ok6(db *pg.DB) { + // ok: pg-sqli + db.Exec(ctx, fmt.Sprintf("SELECT * FROM users WHERE email=hello;")) +} + +func ok7() { + opt, err := pg.ParseURL("postgres://user:pass@localhost:5432/db_name") + if err != nil { + panic(err) + } + + db := pg.Connect(opt) + if _, err := db.Prepare("my-query", "select $1::int"); err != nil { + panic(err) + } + // ok: pg-sqli + row := db.QueryContext(ctx, "my-query", 10) +} diff --git a/go/lang/security/audit/sqli/pg-sqli.yaml b/go/lang/security/audit/sqli/pg-sqli.yaml new file mode 100644 index 00000000..8594696e --- /dev/null +++ b/go/lang/security/audit/sqli/pg-sqli.yaml @@ -0,0 +1,66 @@ +rules: +- id: pg-sqli + languages: + - go + message: >- + Detected string concatenation with a non-literal variable in a go-pg + SQL statement. This could lead to SQL injection if the variable is user-controlled + and not properly sanitized. In order to prevent SQL injection, + use parameterized queries instead of string concatenation. You can use parameterized + queries like so: + '(SELECT ? FROM table, data1)' + metadata: + cwe: + - "CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')" + references: + - https://pg.uptrace.dev/ + - https://pkg.go.dev/github.com/go-pg/pg/v10 + category: security + technology: + - go-pg + confidence: LOW + owasp: + - A01:2017 - Injection + - A03:2021 - Injection + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - vuln + likelihood: LOW + impact: HIGH + severity: ERROR + patterns: + - pattern-either: + - patterns: + - pattern: | + $DB.$METHOD(...,$QUERY,...) + - pattern-either: + - pattern-inside: | + $QUERY = $X + $Y + ... + - pattern-inside: | + $QUERY += $X + ... + - pattern-inside: | + $QUERY = fmt.Sprintf("...", $PARAM1, ...) + ... + - pattern-not-inside: | + $QUERY += "..." + ... + - pattern-not-inside: | + $QUERY = "..." + "..." + ... + - pattern: $DB.$METHOD(..., $X + $Y, ...) + - pattern: $DB.$METHOD(..., fmt.Sprintf("...", $PARAM1, ...), ...) + - pattern-either: + - pattern-inside: | + $DB = pg.Connect(...) + ... + - pattern-inside: | + func $FUNCNAME(..., $DB *pg.DB, ...) { + ... + } + - pattern-not: $DB.$METHOD(..., "..." + "...", ...) + - metavariable-regex: + metavariable: $METHOD + regex: ^(Exec|ExecContext|ExecOne|ExecOneContext|Query|QueryOne|QueryContext|QueryOneContext)$ diff --git a/go/lang/security/audit/sqli/pgx-sqli.go b/go/lang/security/audit/sqli/pgx-sqli.go new file mode 100644 index 00000000..7c86b0f8 --- /dev/null +++ b/go/lang/security/audit/sqli/pgx-sqli.go @@ -0,0 +1,121 @@ +package main + +import "database/sql" +import "fmt" + +func bad1() { + pgxConfig := pgx.ConnConfig{ + Host: "localhost", + Database: "quetest", + User: "quetest", + } + pgxConnPoolConfig := pgx.ConnPoolConfig{pgxConfig, 3, nil} + conn, err := pgx.NewConnPool(pgxConnPoolConfig) + if err != nil { + log.Fatal(err) + } + query = "SELECT name FROM users WHERE age=" + req.FormValue("age") + // ruleid: pgx-sqli + rows, err := conn.Query(query) +} + +func bad2() { + conn, err := pgx.Connect(context.Background(), os.Getenv("DATABASE_URL")) + if err != nil { + panic(err) + } + query = "SELECT name FROM users WHERE age=" + req.FormValue("age") + // ruleid: pgx-sqli + conn.QueryEx(query) +} + +func bad3() { + config, err := pgx.ParseConfig(os.Getenv("DATABASE_URL")) + if err != nil { + panic(err) + } + config.Logger = log15adapter.NewLogger(log.New("module", "pgx")) + + conn, err := pgx.ConnectConfig(context.Background(), config) + + query = "SELECT name FROM users WHERE age=" + query += req.FormValue("age") + // ruleid: pgx-sqli + conn.QueryRow(query) +} + +func bad4(conn *pgx.Conn) { + query = fmt.Sprintf("SELECT * FROM users WHERE email='%s';", email) + // ruleid: pgx-sqli + conn.Exec(query) +} + +func bad4(conn *pgx.Conn) { + // ruleid: pgx-sqli + conn.Exec("SELECT name FROM users WHERE age=" + req.FormValue("age")) +} + +func bad5(conn *pgx.Conn) { + // ruleid: pgx-sqli + conn.ExecEx(fmt.Sprintf("SELECT * FROM users WHERE email='%s';", email)) +} + +func ok1(conn *pgx.Conn) { + query = fmt.Sprintf("SELECT * FROM users WHERE email=hello;") + // ok: pgx-sqli + conn.QueryRowEx(query) +} + +func ok2(conn *pgx.Conn) { + query = "SELECT name FROM users WHERE age=" + "3" + // ok: pgx-sqli + conn.Query(query) +} + +func ok3(conn *pgx.Conn) { + query = "SELECT name FROM users WHERE age=" + query += "3" + // ok: pgx-sqli + conn.QueryRow(query) +} + +func ok4(conn *pgx.Conn) { + // ok: pgx-sqli + conn.Exec("INSERT INTO users(name, email) VALUES($1, $2)", + "Jon Calhoun", "jon@calhoun.io") +} + +func ok5(conn *pgx.Conn) { + // ok: pgx-sqli + conn.Exec("SELECT name FROM users WHERE age=" + "3") +} + +func ok6(conn *pgx.Conn) { + // ok: pgx-sqli + conn.Exec(fmt.Sprintf("SELECT * FROM users WHERE email=hello;")) +} + +func ok7() { + conf := pgx.ConnPoolConfig{ + ConnConfig: pgx.ConnConfig{ + Host: "/run/postgresql", + User: "postgres", + Database: "test", + }, + MaxConnections: 5, + } + db, err := pgx.NewConnPool(conf) + if err != nil { + panic(err) + } + if _, err := db.Prepare("my-query", "select $1::int"); err != nil { + panic(err) + } + // ok: pgx-sqli + row := db.QueryRow("my-query", 10) + var i int + if err := row.Scan(&i); err != nil { + panic(err) + } + fmt.Println(i) +} diff --git a/go/lang/security/audit/sqli/pgx-sqli.yaml b/go/lang/security/audit/sqli/pgx-sqli.yaml new file mode 100644 index 00000000..3d625682 --- /dev/null +++ b/go/lang/security/audit/sqli/pgx-sqli.yaml @@ -0,0 +1,70 @@ +rules: +- id: pgx-sqli + languages: + - go + message: >- + Detected string concatenation with a non-literal variable in a pgx + Go SQL statement. This could lead to SQL injection if the variable is user-controlled + and not properly sanitized. In order to prevent SQL injection, + use parameterized queries instead. You can use parameterized queries like so: + (`SELECT $1 FROM table`, `data1) + metadata: + cwe: + - "CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')" + references: + - https://github.com/jackc/pgx + - https://pkg.go.dev/github.com/jackc/pgx/v4#hdr-Connection_Pool + category: security + technology: + - pgx + confidence: LOW + owasp: + - A01:2017 - Injection + - A03:2021 - Injection + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - vuln + likelihood: LOW + impact: HIGH + patterns: + - pattern-either: + - patterns: + - pattern: $DB.$METHOD(...,$QUERY,...) + - pattern-either: + - pattern-inside: | + $QUERY = $X + $Y + ... + - pattern-inside: | + $QUERY += $X + ... + - pattern-inside: | + $QUERY = fmt.Sprintf("...", $PARAM1, ...) + ... + - pattern-not-inside: | + $QUERY += "..." + ... + - pattern-not-inside: | + $QUERY = "..." + "..." + ... + - pattern: $DB.$METHOD(..., $X + $Y, ...) + - pattern: $DB.$METHOD(..., fmt.Sprintf("...", $PARAM1, ...), ...) + - pattern-either: + - pattern-inside: | + $DB, ... = pgx.Connect(...) + ... + - pattern-inside: | + $DB, ... = pgx.NewConnPool(...) + ... + - pattern-inside: | + $DB, ... = pgx.ConnectConfig(...) + ... + - pattern-inside: | + func $FUNCNAME(..., $DB *pgx.Conn, ...) { + ... + } + - pattern-not: $DB.$METHOD(..., "..." + "...", ...) + - metavariable-regex: + metavariable: $METHOD + regex: ^(Exec|ExecEx|Query|QueryEx|QueryRow|QueryRowEx)$ + severity: ERROR diff --git a/go/lang/security/audit/unsafe-reflect-by-name.go b/go/lang/security/audit/unsafe-reflect-by-name.go new file mode 100644 index 00000000..7009e5ee --- /dev/null +++ b/go/lang/security/audit/unsafe-reflect-by-name.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + "reflect" +) + +func (mf mapFmt) Format(s fmt.State, c rune, userInput string) { + refVal := mf.m + key := keys[i] + val := refVal.MapIndex(key) + + // ruleid: unsafe-reflect-by-name + meth := key.MethodByName(userInput) + meth.Call(nil)[0] + + return +} + +func Test1(job interface{}, userInput string) { + jobData := make(map[string]interface{}) + + valueJ := reflect.ValueOf(job).Elem() + + // ruleid: unsafe-reflect-by-name + jobData["color"] = valueJ.FieldByName(userInput).String() + + return jobData +} + +func OkTest(job interface{}, userInput string) { + jobData := make(map[string]interface{}) + + valueJ := reflect.ValueOf(job).Elem() + + // ok: unsafe-reflect-by-name + meth := valueJ.MethodByName("Name") + // ok: unsafe-reflect-by-name + jobData["color"] = valueJ.FieldByName("color").String() + + return jobData +} diff --git a/go/lang/security/audit/unsafe-reflect-by-name.yaml b/go/lang/security/audit/unsafe-reflect-by-name.yaml new file mode 100644 index 00000000..5f4879fd --- /dev/null +++ b/go/lang/security/audit/unsafe-reflect-by-name.yaml @@ -0,0 +1,42 @@ +rules: +- id: unsafe-reflect-by-name + patterns: + - pattern-either: + - pattern: | + $SMTH.MethodByName($NAME,...) + - pattern: | + $SMTH.FieldByName($NAME,...) + - pattern-not: | + $SMTH.MethodByName("...",...) + - pattern-not: | + $SMTH.FieldByName("...",...) + - pattern-inside: | + import "reflect" + ... + message: >- + If an attacker can supply values that the application then uses to determine which + method or field to invoke, + the potential exists for the attacker to create control flow paths through the + application + that were not intended by the application developers. + This attack vector may allow the attacker to bypass authentication or access control + checks + or otherwise cause the application to behave in an unexpected manner. + metadata: + cwe: + - "CWE-470: Use of Externally-Controlled Input to Select Classes or Code ('Unsafe Reflection')" + owasp: + - A03:2021 - Injection + category: security + technology: + - go + confidence: LOW + references: + - https://owasp.org/Top10/A03_2021-Injection + subcategory: + - audit + likelihood: LOW + impact: LOW + severity: WARNING + languages: + - go diff --git a/go/lang/security/audit/unsafe.go b/go/lang/security/audit/unsafe.go new file mode 100644 index 00000000..1922fa3e --- /dev/null +++ b/go/lang/security/audit/unsafe.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "unsafe" + + foobarbaz "unsafe" +) + +type Fake struct{} + +func (Fake) Good() {} +func main() { + unsafeM := Fake{} + unsafeM.Good() + intArray := [...]int{1, 2} + fmt.Printf("\nintArray: %v\n", intArray) + intPtr := &intArray[0] + fmt.Printf("\nintPtr=%p, *intPtr=%d.\n", intPtr, *intPtr) + // ruleid: use-of-unsafe-block + addressHolder := uintptr(foobarbaz.Pointer(intPtr)) + unsafe.Sizeof(intArray[0]) + // ruleid: use-of-unsafe-block + intPtr = (*int)(foobarbaz.Pointer(addressHolder)) + fmt.Printf("\nintPtr=%p, *intPtr=%d.\n\n", intPtr, *intPtr) +} diff --git a/go/lang/security/audit/unsafe.yaml b/go/lang/security/audit/unsafe.yaml new file mode 100644 index 00000000..f1fda274 --- /dev/null +++ b/go/lang/security/audit/unsafe.yaml @@ -0,0 +1,24 @@ +rules: +- id: use-of-unsafe-block + message: >- + Using the unsafe package in Go gives you low-level memory management and many of the strengths of + the C language, but also steps around the type safety of Go and can lead to buffer overflows and + possible arbitrary code execution by an attacker. + Only use this package if you absolutely know what you're doing. + languages: [go] + severity: WARNING + metadata: + cwe: + - 'CWE-242: Use of Inherently Dangerous Function' + source_rule_url: https://github.com/securego/gosec/blob/master/rules/unsafe.go + category: security + technology: + - go + confidence: LOW + references: + - https://cwe.mitre.org/data/definitions/242.html + subcategory: + - audit + likelihood: LOW + impact: LOW + pattern: unsafe.$FUNC(...) \ No newline at end of file diff --git a/go/lang/security/audit/xss/import-text-template.fixed.go b/go/lang/security/audit/xss/import-text-template.fixed.go new file mode 100644 index 00000000..d406047a --- /dev/null +++ b/go/lang/security/audit/xss/import-text-template.fixed.go @@ -0,0 +1,53 @@ +// cf. https://www.veracode.com/blog/secure-development/use-golang-these-mistakes-could-compromise-your-apps-security + +package main + +import ( + "net/http" + // ruleid: import-text-template + "html/template" + "encoding/json" + "io/ioutil" + "os" +) + +const tmpl = "" + +type TodoPageData struct { + PageTitle string + Todos []Todo +} + +type Todo struct { + Title string "json:title" + Done bool "json:done" +} + +func (t Todo) ToString() string { + bytes, _ := json.Marshal(t) + return string(bytes) +} + +func getTodos() []Todo { + todos := make([]Todo, 3) + raw, _ := ioutil.ReadFile("./todos.json") + json.Unmarshal(raw, &todos) + return todos + +} + +func main() { + tmpl := template.Must(template.ParseFiles("index.html")) + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + data := TodoPageData { + PageTitle: "My Todos!", + Todos: getTodos(), + } + + tmpl.Execute(w, data) + + }) + + http.ListenAndServe(":" + os.Getenv("PORT"), nil) +} diff --git a/go/lang/security/audit/xss/import-text-template.go b/go/lang/security/audit/xss/import-text-template.go new file mode 100644 index 00000000..9b3f74bc --- /dev/null +++ b/go/lang/security/audit/xss/import-text-template.go @@ -0,0 +1,53 @@ +// cf. https://www.veracode.com/blog/secure-development/use-golang-these-mistakes-could-compromise-your-apps-security + +package main + +import ( + "net/http" + // ruleid: import-text-template + "text/template" + "encoding/json" + "io/ioutil" + "os" +) + +const tmpl = "" + +type TodoPageData struct { + PageTitle string + Todos []Todo +} + +type Todo struct { + Title string "json:title" + Done bool "json:done" +} + +func (t Todo) ToString() string { + bytes, _ := json.Marshal(t) + return string(bytes) +} + +func getTodos() []Todo { + todos := make([]Todo, 3) + raw, _ := ioutil.ReadFile("./todos.json") + json.Unmarshal(raw, &todos) + return todos + +} + +func main() { + tmpl := template.Must(template.ParseFiles("index.html")) + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + data := TodoPageData { + PageTitle: "My Todos!", + Todos: getTodos(), + } + + tmpl.Execute(w, data) + + }) + + http.ListenAndServe(":" + os.Getenv("PORT"), nil) +} diff --git a/go/lang/security/audit/xss/import-text-template.yaml b/go/lang/security/audit/xss/import-text-template.yaml new file mode 100644 index 00000000..fe37bcce --- /dev/null +++ b/go/lang/security/audit/xss/import-text-template.yaml @@ -0,0 +1,42 @@ +rules: +- id: import-text-template + message: >- + When working with web applications that involve rendering user-generated + content, it's important to properly escape any HTML content to prevent + Cross-Site Scripting (XSS) attacks. In Go, the `text/template` package does + not automatically escape HTML content, which can leave your application + vulnerable to these types of attacks. To mitigate this risk, it's + recommended to use the `html/template` package instead, which provides + built-in functionality for HTML escaping. By using `html/template` to render + your HTML content, you can help to ensure that your web application is more + secure and less susceptible to XSS vulnerabilities. + metadata: + owasp: + - A07:2017 - Cross-Site Scripting (XSS) + - A03:2021 - Injection + cwe: + - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')" + references: + - https://www.veracode.com/blog/secure-development/use-golang-these-mistakes-could-compromise-your-apps-security + category: security + technology: + - go + confidence: LOW + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - audit + likelihood: LOW + impact: LOW + severity: WARNING + patterns: + - pattern: | + import "$IMPORT" + - metavariable-regex: + metavariable: $IMPORT + regex: ^(text/template)$ + - focus-metavariable: $IMPORT + fix: | + html/template + languages: + - go diff --git a/go/lang/security/audit/xss/no-direct-write-to-responsewriter.go b/go/lang/security/audit/xss/no-direct-write-to-responsewriter.go new file mode 100644 index 00000000..53f9f66f --- /dev/null +++ b/go/lang/security/audit/xss/no-direct-write-to-responsewriter.go @@ -0,0 +1,77 @@ +package main + +import ( + "fmt" + "log" + "net/http" +) + +func getMovieQuote() map[string]string { + m := make(map[string]string) + m["quote"] = "I'll be back." + m["movie"] = "The Terminator" + m["year"] = "1984" + + return m +} + +func healthCheck(w http.ResponseWriter, r *http.Request) { + // ok: no-direct-write-to-responsewriter + w.Write([]byte("alive")) +} + +func indexPage(w http.ResponseWriter, r *http.Request) { + const tme = `` + + const template = ` + + +

Random Movie Quotes

+

%s

+

~%s, %s

+ + ` + + quote := getMovieQuote() + + quoteText := quote["quote"] + movie := quote["movie"] + year := quote["year"] + + w.WriteHeader(http.StatusAccepted) + // ruleid: no-direct-write-to-responsewriter + w.Write([]byte(fmt.Sprintf(template, quoteText, movie, year))) +} + +func errorPage(w http.ResponseWriter, r *http.Request) { + params := r.URL.Query() + urls, ok := params["url"] + if !ok { + log.Println("Error") + return + } + url := urls[0] + + const template = ` + + +

error; page not found. go back

+ + ` + + w.WriteHeader(http.StatusAccepted) + // ruleid: no-direct-write-to-responsewriter + w.Write([]byte(fmt.Sprintf(template, url))) +} + +func writeErrorResponse(rw *http.ResponseWriter, status int, body string) { + (*rw).WriteHeader(status) + // ruleid: no-direct-write-to-responsewriter + (*rw).Write([]byte(body)) +} + +func main() { + http.HandleFunc("/", indexPage) + http.HandleFunc("/error", errorPage) + http.ListenAndServe(":8080", nil) +} diff --git a/go/lang/security/audit/xss/no-direct-write-to-responsewriter.yaml b/go/lang/security/audit/xss/no-direct-write-to-responsewriter.yaml new file mode 100644 index 00000000..5577c231 --- /dev/null +++ b/go/lang/security/audit/xss/no-direct-write-to-responsewriter.yaml @@ -0,0 +1,46 @@ +rules: +- id: no-direct-write-to-responsewriter + languages: + - go + message: >- + Detected directly writing or similar in 'http.ResponseWriter.write()'. + This bypasses HTML escaping that prevents cross-site scripting + vulnerabilities. Instead, use the 'html/template' package + and render data using 'template.Execute()'. + metadata: + category: security + cwe: + - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')" + owasp: + - A07:2017 - Cross-Site Scripting (XSS) + - A03:2021 - Injection + references: + - https://blogtitle.github.io/robn-go-security-pearls-cross-site-scripting-xss/ + technology: + - go + confidence: LOW + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - audit + likelihood: LOW + impact: MEDIUM + patterns: + - pattern-either: + - pattern-inside: | + func $HANDLER(..., $WRITER http.ResponseWriter, ...) { + ... + } + - pattern-inside: | + func $HANDLER(..., $WRITER *http.ResponseWriter, ...) { + ... + } + - pattern-inside: | + func(..., $WRITER http.ResponseWriter, ...) { + ... + } + - pattern-either: + - pattern: $WRITER.Write(...) + - pattern: (*$WRITER).Write(...) + - pattern-not: $WRITER.Write([]byte("...")) + severity: WARNING diff --git a/go/lang/security/audit/xss/no-fprintf-to-responsewriter.go b/go/lang/security/audit/xss/no-fprintf-to-responsewriter.go new file mode 100644 index 00000000..55a3265f --- /dev/null +++ b/go/lang/security/audit/xss/no-fprintf-to-responsewriter.go @@ -0,0 +1,52 @@ +// cf. https://blogtitle.github.io/robn-go-security-pearls-cross-site-scripting-xss/ + +package main + +import ( + "fmt" + "net/http" +) + + +func isValid(token string) bool { + return true +} + +func vulnerableHandler(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + tok := r.FormValue("token") + if !isValid(tok) { + // ruleid:no-fprintf-to-responsewriter + fmt.Fprintf(w, "Invalid token: %q", tok) + } + // ... +} + +// cf. https://github.com/wrfly/container-web-tty//blob/09f891f0d12d0a930f37b675e2eda5784733579a/route/asset/bindata.go#L242 +func dirList(w http.ResponseWriter, r *http.Request, f http.File) { + dirs, err := f.Readdir(-1) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + // ok:no-fprintf-to-responsewriter + fmt.Fprint(w, "Error reading directory") + return + } + sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() }) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + // ok:no-fprintf-to-responsewriter + fmt.Fprintf(w, "
\n")
+  for _, d := range dirs {
+    name := d.Name()
+    if d.IsDir() {
+      name += "/"
+    }
+    // name may contain '?' or '#', which must be escaped to remain
+    // part of the URL path, and not indicate the start of a query
+    // string or fragment.
+    url := url.URL{Path: filepath.Join(r.RequestURI, name)}
+    // ruleid:no-fprintf-to-responsewriter
+    fmt.Fprintf(w, "%s\n", url.String(), d.Name())
+  }
+  // ok:no-fprintf-to-responsewriter
+  fmt.Fprintf(w, "
\n") +} diff --git a/go/lang/security/audit/xss/no-fprintf-to-responsewriter.yaml b/go/lang/security/audit/xss/no-fprintf-to-responsewriter.yaml new file mode 100644 index 00000000..c6be9d36 --- /dev/null +++ b/go/lang/security/audit/xss/no-fprintf-to-responsewriter.yaml @@ -0,0 +1,40 @@ +rules: +- id: no-fprintf-to-responsewriter + message: >- + Detected 'Fprintf' or similar writing to 'http.ResponseWriter'. + This bypasses HTML escaping that prevents cross-site scripting + vulnerabilities. Instead, use the 'html/template' package + to render data to users. + metadata: + owasp: + - A07:2017 - Cross-Site Scripting (XSS) + - A03:2021 - Injection + cwe: + - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')" + references: + - https://blogtitle.github.io/robn-go-security-pearls-cross-site-scripting-xss/ + category: security + technology: + - go + confidence: LOW + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - audit + likelihood: LOW + impact: MEDIUM + severity: WARNING + patterns: + - pattern-either: + - pattern-inside: | + func $HANDLER(..., $WRITER http.ResponseWriter, ...) { + ... + } + - pattern-inside: | + func(..., $WRITER http.ResponseWriter, ...) { + ... + } + - pattern-not: fmt.$PRINTF($WRITER, "...") + - pattern: fmt.$PRINTF($WRITER, ...) + languages: + - go diff --git a/go/lang/security/audit/xss/no-interpolation-in-tag.html b/go/lang/security/audit/xss/no-interpolation-in-tag.html new file mode 100644 index 00000000..262d656b --- /dev/null +++ b/go/lang/security/audit/xss/no-interpolation-in-tag.html @@ -0,0 +1,27 @@ +

From: {{.from_email}}

+

To: {{.recipient}}

+

Subject: {{.subject}}

+ + +
diff --git a/go/lang/security/audit/xss/no-interpolation-in-tag.yaml b/go/lang/security/audit/xss/no-interpolation-in-tag.yaml new file mode 100644 index 00000000..c1a4beef --- /dev/null +++ b/go/lang/security/audit/xss/no-interpolation-in-tag.yaml @@ -0,0 +1,38 @@ +rules: +- id: no-interpolation-in-tag + message: >- + Detected template variable interpolation in an HTML tag. + This is potentially vulnerable to cross-site scripting (XSS) + attacks because a malicious actor has control over HTML + but without the need to use escaped characters. Use explicit + tags instead. + metadata: + cwe: + - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')" + owasp: + - A07:2017 - Cross-Site Scripting (XSS) + - A03:2021 - Injection + references: + - https://github.com/golang/go/issues/19669 + - https://blogtitle.github.io/robn-go-security-pearls-cross-site-scripting-xss/ + category: security + technology: + - generic + confidence: LOW + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - audit + likelihood: LOW + impact: MEDIUM + languages: + - generic + severity: WARNING + paths: + include: + - '*.html' + - '*.thtml' + - '*.gohtml' + - '*.tmpl' + - '*.tpl' + pattern: <{{ ... }} ... > diff --git a/go/lang/security/audit/xss/no-interpolation-js-template-string.html b/go/lang/security/audit/xss/no-interpolation-js-template-string.html new file mode 100644 index 00000000..456bf74b --- /dev/null +++ b/go/lang/security/audit/xss/no-interpolation-js-template-string.html @@ -0,0 +1,23 @@ +

From: {{.from_email}}

+

To: {{.recipient}}

+

Subject: {{.subject}}

+ + +
+ + diff --git a/go/lang/security/audit/xss/no-interpolation-js-template-string.yaml b/go/lang/security/audit/xss/no-interpolation-js-template-string.yaml new file mode 100644 index 00000000..9015c722 --- /dev/null +++ b/go/lang/security/audit/xss/no-interpolation-js-template-string.yaml @@ -0,0 +1,42 @@ +rules: +- id: no-interpolation-js-template-string + message: >- + Detected template variable interpolation in a JavaScript + template string. This is potentially vulnerable to + cross-site scripting (XSS) attacks because a malicious + actor has control over JavaScript but without the need + to use escaped characters. Instead, obtain this variable + outside of the template string and ensure your template + is properly escaped. + metadata: + cwe: + - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')" + owasp: + - A07:2017 - Cross-Site Scripting (XSS) + - A03:2021 - Injection + references: + - https://github.com/golang/go/issues/9200#issuecomment-66100328 + - https://blogtitle.github.io/robn-go-security-pearls-cross-site-scripting-xss/ + category: security + technology: + - generic + confidence: LOW + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - audit + likelihood: LOW + impact: MEDIUM + languages: + - generic + severity: WARNING + paths: + include: + - '*.html' + - '*.thtml' + - '*.gohtml' + - '*.tmpl' + - '*.tpl' + patterns: + - pattern-inside: + - pattern: '` ... {{ ... }} ...`' diff --git a/go/lang/security/audit/xss/no-io-writestring-to-responsewriter.go b/go/lang/security/audit/xss/no-io-writestring-to-responsewriter.go new file mode 100644 index 00000000..df00f3aa --- /dev/null +++ b/go/lang/security/audit/xss/no-io-writestring-to-responsewriter.go @@ -0,0 +1,31 @@ +// cf. https://blogtitle.github.io/robn-go-security-pearls-cross-site-scripting-xss/ + +package main + +import ( + "fmt" + "net/http" +) + + +func isValid(token string) bool { + return true +} + +func vulnerableHandler(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + tok := r.FormValue("token") + if !isValid(tok) { + // ruleid:no-io-writestring-to-responsewriter + io.WriteString(w, fmt.Sprintf("Invalid token: %q", tok)) + } + // ... +} + +// cf. https://github.com/hashicorp/vault-plugin-database-mongodbatlas//blob/9cf156a44f9c8d56fb263f692541e5c7fbab9ab1/vendor/golang.org/x/net/http2/server.go#L2160 +func handleHeaderListTooLong(w http.ResponseWriter, r *http.Request) { + const statusRequestHeaderFieldsTooLarge = 431 + w.WriteHeader(statusRequestHeaderFieldsTooLarge) + // ok:no-io-writestring-to-responsewriter + io.WriteString(w, "

HTTP Error 431

Request Header Field(s) Too Large

") +} diff --git a/go/lang/security/audit/xss/no-io-writestring-to-responsewriter.yaml b/go/lang/security/audit/xss/no-io-writestring-to-responsewriter.yaml new file mode 100644 index 00000000..a359cf05 --- /dev/null +++ b/go/lang/security/audit/xss/no-io-writestring-to-responsewriter.yaml @@ -0,0 +1,41 @@ +rules: +- id: no-io-writestring-to-responsewriter + message: >- + Detected 'io.WriteString()' writing directly to 'http.ResponseWriter'. + This bypasses HTML escaping that prevents cross-site scripting + vulnerabilities. Instead, use the 'html/template' package + to render data to users. + metadata: + owasp: + - A07:2017 - Cross-Site Scripting (XSS) + - A03:2021 - Injection + cwe: + - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')" + references: + - https://blogtitle.github.io/robn-go-security-pearls-cross-site-scripting-xss/ + - https://golang.org/pkg/io/#WriteString + category: security + technology: + - go + confidence: LOW + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - audit + likelihood: LOW + impact: MEDIUM + severity: WARNING + patterns: + - pattern-either: + - pattern-inside: | + func $HANDLER(..., $WRITER http.ResponseWriter, ...) { + ... + } + - pattern-inside: | + func(..., $WRITER http.ResponseWriter, ...) { + ... + } + - pattern-not: io.WriteString($WRITER, "...") + - pattern: io.WriteString($WRITER, $STRING) + languages: + - go diff --git a/go/lang/security/audit/xss/no-printf-in-responsewriter.go b/go/lang/security/audit/xss/no-printf-in-responsewriter.go new file mode 100644 index 00000000..036684f9 --- /dev/null +++ b/go/lang/security/audit/xss/no-printf-in-responsewriter.go @@ -0,0 +1,66 @@ +package main + +import ( + "fmt" + "log" + "net/http" +) + +func getMovieQuote() map[string]string { + m := make(map[string]string) + m["quote"] = "I'll be back." + m["movie"] = "The Terminator" + m["year"] = "1984" + + return m +} + +func indexPage(w http.ResponseWriter, r *http.Request) { + const tme = `` + + const template = ` + + +

Random Movie Quotes

+

%s

+

~%s, %s

+ + ` + + quote := getMovieQuote() + + quoteText := quote["quote"] + movie := quote["movie"] + year := quote["year"] + + w.WriteHeader(http.StatusAccepted) + // ruleid: no-printf-in-responsewriter + w.Write([]byte(fmt.Sprintf(template, quoteText, movie, year))) +} + +func errorPage(w http.ResponseWriter, r *http.Request) { + params := r.URL.Query() + urls, ok := params["url"] + if !ok { + log.Println("Error") + return + } + url := urls[0] + + const template = ` + + +

error; page not found. go back

+ + ` + + w.WriteHeader(http.StatusAccepted) + // ruleid: no-printf-in-responsewriter + w.Write([]byte(fmt.Sprintf(template, url))) +} + +func main() { + http.HandleFunc("/", indexPage) + http.HandleFunc("/error", errorPage) + http.ListenAndServe(":8080", nil) +} diff --git a/go/lang/security/audit/xss/no-printf-in-responsewriter.yaml b/go/lang/security/audit/xss/no-printf-in-responsewriter.yaml new file mode 100644 index 00000000..22ae6872 --- /dev/null +++ b/go/lang/security/audit/xss/no-printf-in-responsewriter.yaml @@ -0,0 +1,40 @@ +rules: +- id: no-printf-in-responsewriter + message: >- + Detected 'printf' or similar in 'http.ResponseWriter.write()'. + This bypasses HTML escaping that prevents cross-site scripting + vulnerabilities. Instead, use the 'html/template' package + to render data to users. + metadata: + owasp: + - A07:2017 - Cross-Site Scripting (XSS) + - A03:2021 - Injection + cwe: + - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')" + references: + - https://blogtitle.github.io/robn-go-security-pearls-cross-site-scripting-xss/ + category: security + technology: + - go + confidence: LOW + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - audit + likelihood: LOW + impact: MEDIUM + severity: WARNING + patterns: + - pattern-either: + - pattern-inside: | + func $HANDLER(..., $WRITER http.ResponseWriter, ...) { + ... + } + - pattern-inside: | + func(..., $WRITER http.ResponseWriter, ...) { + ... + } + - pattern: | + $WRITER.Write(<... fmt.$PRINTF(...) ...>, ...) + languages: + - go diff --git a/go/lang/security/audit/xss/template-html-does-not-escape.go b/go/lang/security/audit/xss/template-html-does-not-escape.go new file mode 100644 index 00000000..4f80a792 --- /dev/null +++ b/go/lang/security/audit/xss/template-html-does-not-escape.go @@ -0,0 +1,101 @@ +package main + +import ( + "fmt" + "html/template" + "net/http" + "strconv" +) + +func Fine(r *http.Request) template.HTML { + // ok: unsafe-template-type + return template.HTML("

Hello, world

") +} + +func AlsoFine(r *http.Request) template.HTML { + // ok: unsafe-template-type + return template.HTML("

" + "Hello, world

") +} + +func OthersThatAreFine(r *http.Request) template.HTML { + // ok: unsafe-template-type + a := template.HTMLAttr("

Hello, world

") + // ok: unsafe-template-type + a := template.JS("

Hello, world

") + // ok: unsafe-template-type + a := template.URL("

Hello, world

") + // ok: unsafe-template-type + a := template.CSS("

Hello, world

") + // ok: unsafe-template-type + a := template.Srcset("

Hello, world

") +} + +func OthersThatAreNOTFine(r *http.Request, data string) template.HTML { + // ruleid: unsafe-template-type + a := template.HTMLAttr(fmt.Sprintf("

%s

", data)) + // ruleid: unsafe-template-type + a := template.JS(fmt.Sprintf("

%s

", data)) + // ruleid: unsafe-template-type + a := template.URL(fmt.Sprintf("

%s

", data)) + // ruleid: unsafe-template-type + a := template.CSS(fmt.Sprintf("

%s

", data)) + // ruleid: unsafe-template-type + a := template.Srcset(fmt.Sprintf("

%s

", data)) +} + +func Concat(r *http.Request) template.HTML { + customerId := r.URL.Query().Get("id") + tmpl := "

" + customerId + "

" + + // ruleid: unsafe-template-type + return template.HTML(tmpl) +} + +func ConcatBranch(r *http.Request) template.HTML { + customerId := r.URL.Query().Get("id") + doIt, err := strconv.ParseBool(r.URL.Query().Get("do")) + if err != nil { + return template.HTML("") + } + var tmpl string + if doIt { + tmpl = "

" + customerId + "

" + } else { + tmpl = "" + } + + // ruleid: unsafe-template-type + return template.HTML(tmpl) +} + +func ConcatInline(r *http.Request) template.HTML { + customerId := r.URL.Query().Get("id") + + // ruleid: unsafe-template-type + return template.HTML("

" + customerId + "

") +} + +func ConcatInlineOneside(r *http.Request) template.HTML { + customerId := r.URL.Query().Get("id") + + // ruleid: unsafe-template-type + return template.HTML("

" + customerId) +} + +func Formatted(r *http.Request) template.HTML { + customerId := r.URL.Query().Get("id") + tmpl, err := fmt.Printf("

%s

", customerId) + if err != nil { + return template.HTML("") + } + // ruleid: unsafe-template-type + return template.HTML(tmpl) +} + +func FormattedInline(r *http.Request) template.HTML { + customerId := r.URL.Query().Get("id") + // ruleid: unsafe-template-type + return template.HTML(fmt.Sprintf("

%s

", customerId)) +} + +func main() {} diff --git a/go/lang/security/audit/xss/template-html-does-not-escape.yaml b/go/lang/security/audit/xss/template-html-does-not-escape.yaml new file mode 100644 index 00000000..031ae966 --- /dev/null +++ b/go/lang/security/audit/xss/template-html-does-not-escape.yaml @@ -0,0 +1,41 @@ +rules: +- id: unsafe-template-type + message: >- + Semgrep could not determine that the argument to 'template.HTML()' + is a constant. 'template.HTML()' and similar does not escape contents. + Be absolutely sure there is no user-controlled data in this + template. If user data can reach this template, you may have + a XSS vulnerability. Instead, do not use this function and + use 'template.Execute()'. + metadata: + cwe: + - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')" + owasp: + - A07:2017 - Cross-Site Scripting (XSS) + - A03:2021 - Injection + references: + - https://golang.org/pkg/html/template/#HTML + - https://github.com/0c34/govwa/blob/139693e56406b5684d2a6ae22c0af90717e149b8/vulnerability/xss/xss.go#L33 + category: security + technology: + - go + confidence: LOW + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - audit + likelihood: LOW + impact: MEDIUM + languages: [go] + severity: WARNING + patterns: + - pattern-not: template.$ANY("..." + "...") + - pattern-not: template.$ANY("...") + - pattern-either: + - pattern: template.HTML(...) + - pattern: template.CSS(...) + - pattern: template.HTMLAttr(...) + - pattern: template.JS(...) + - pattern: template.JSStr(...) + - pattern: template.Srcset(...) + - pattern: template.URL(...) diff --git a/go/lang/security/audit/xxe/parsing-external-entities-enabled.go b/go/lang/security/audit/xxe/parsing-external-entities-enabled.go new file mode 100644 index 00000000..269d59e7 --- /dev/null +++ b/go/lang/security/audit/xxe/parsing-external-entities-enabled.go @@ -0,0 +1,30 @@ +import ( + "fmt" + "github.com/lestrrat-go/libxml2/parser" +) + +func vuln() { + const s = "]>&e;" + // ruleid: parsing-external-entities-enabled + p := parser.New(parser.XMLParseNoEnt) + doc, err := p.ParseString(s) + if err != nil { + fmt.Println(err) + return + } + fmt.Println("Doc successfully parsed!") + fmt.Println(doc) +} + +func not_vuln() { + const s = "]>&e;" + // ok: parsing-external-entities-enabled + p := parser.New() + doc, err := p.ParseString(s) + if err != nil { + fmt.Println(err) + return + } + fmt.Println("Doc successfully parsed!") + fmt.Println(doc) +} \ No newline at end of file diff --git a/go/lang/security/audit/xxe/parsing-external-entities-enabled.yaml b/go/lang/security/audit/xxe/parsing-external-entities-enabled.yaml new file mode 100644 index 00000000..d0ff2178 --- /dev/null +++ b/go/lang/security/audit/xxe/parsing-external-entities-enabled.yaml @@ -0,0 +1,33 @@ +rules: +- id: parsing-external-entities-enabled + patterns: + - pattern-inside: | + import ("github.com/lestrrat-go/libxml2/parser") + ... + - pattern: $PARSER := parser.New(parser.XMLParseNoEnt) + message: >- + Detected enabling of "XMLParseNoEnt", which allows parsing of external entities and can lead to XXE + if user controlled data is parsed by the library. Instead, do not enable "XMLParseNoEnt" or be sure + to adequately sanitize user-controlled data when it is being parsed by this library. + languages: + - go + severity: WARNING + metadata: + category: security + cwe: + - 'CWE-611: Improper Restriction of XML External Entity Reference' + owasp: + - A04:2017 - XML External Entities (XXE) + - A05:2021 - Security Misconfiguration + references: + - https://knowledge-base.secureflag.com/vulnerabilities/xml_injection/xml_entity_expansion_go_lang.html + - https://owasp.org/www-community/vulnerabilities/XML_External_Entity_(XXE)_Processing + technology: + - libxml2 + confidence: LOW + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - audit + likelihood: LOW + impact: HIGH diff --git a/go/lang/security/bad_tmp.go b/go/lang/security/bad_tmp.go new file mode 100644 index 00000000..92b41c1f --- /dev/null +++ b/go/lang/security/bad_tmp.go @@ -0,0 +1,21 @@ +package unzip + +import ( + "fmt" + "io/ioutil" +) + +func main() { + // ruleid:bad-tmp-file-creation + err := ioutil.WriteFile("/tmp/demo2", []byte("This is some data"), 0644) + if err != nil { + fmt.Println("Error while writing!") + } +} +func main_good() { + // ok:bad-tmp-file-creation + err := ioutil.Tempfile("/tmp", "my_temp") + if err != nil { + fmt.Println("Error while writing!") + } +} diff --git a/go/lang/security/bad_tmp.yaml b/go/lang/security/bad_tmp.yaml new file mode 100644 index 00000000..4633df7e --- /dev/null +++ b/go/lang/security/bad_tmp.yaml @@ -0,0 +1,24 @@ +rules: +- id: bad-tmp-file-creation + message: File creation in shared tmp directory without using ioutil.Tempfile + languages: [go] + severity: WARNING + metadata: + cwe: + - 'CWE-377: Insecure Temporary File' + source-rule-url: https://github.com/securego/gosec + category: security + technology: + - go + confidence: LOW + owasp: + - A01:2021 - Broken Access Control + references: + - https://owasp.org/Top10/A01_2021-Broken_Access_Control + subcategory: + - audit + likelihood: LOW + impact: LOW + pattern-either: + - pattern: ioutil.WriteFile("=~//tmp/.*$/", ...) + - pattern: os.Create("=~//tmp/.*$/", ...) diff --git a/go/lang/security/decompression_bomb.go b/go/lang/security/decompression_bomb.go new file mode 100644 index 00000000..f1d38dd9 --- /dev/null +++ b/go/lang/security/decompression_bomb.go @@ -0,0 +1,103 @@ +// cf. https://github.com/securego/gosec/blob/master/testutils/source.go#L684 + +package unzip + +import ( + "bytes" + "compress/zlib" + "io" + "os" +) + +func blah() { + buff := []byte{120, 156, 202, 72, 205, 201, 201, 215, 81, 40, 207, + 47, 202, 73, 225, 2, 4, 0, 0, 255, 255, 33, 231, 4, 147} + b := bytes.NewReader(buff) + r, err := zlib.NewReader(b) + if err != nil { + panic(err) + } + // ruleid: potential-dos-via-decompression-bomb + _, err := io.Copy(os.Stdout, r) + if err != nil { + panic(err) + } + r.Close() +} + +func blah2() { + buff := []byte{120, 156, 202, 72, 205, 201, 201, 215, 81, 40, 207, + 47, 202, 73, 225, 2, 4, 0, 0, 255, 255, 33, 231, 4, 147} + b := bytes.NewReader(buff) + r, err := zlib.NewReader(b) + if err != nil { + panic(err) + } + buf := make([]byte, 8) + // ruleid: potential-dos-via-decompression-bomb + _, err := io.CopyBuffer(os.Stdout, r, buf) + if err != nil { + panic(err) + } + r.Close() +} + +func blah3() { + r, err := zip.OpenReader("tmp.zip") + if err != nil { + panic(err) + } + defer r.Close() + for i, f := range r.File { + out, err := os.OpenFile("output"+strconv.Itoa(i), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + panic(err) + } + rc, err := f.Open() + if err != nil { + panic(err) + } + // ruleid: potential-dos-via-decompression-bomb + _, err = io.Copy(out, rc) + out.Close() + rc.Close() + if err != nil { + panic(err) + } + } +} + +func benign() { + s, err := os.Open("src") + if err != nil { + panic(err) + } + defer s.Close() + d, err := os.Create("dst") + if err != nil { + panic(err) + } + defer d.Close() + // ok: potential-dos-via-decompression-bomb + _, err = io.Copy(d, s) + if err != nil { + panic(err) + } +} + +func ok() { + buff := []byte{120, 156, 202, 72, 205, 201, 201, 215, 81, 40, 207, + 47, 202, 73, 225, 2, 4, 0, 0, 255, 255, 33, 231, 4, 147} + b := bytes.NewReader(buff) + r, err := zlib.NewReader(b) + if err != nil { + panic(err) + } + buf := make([]byte, 8) + // ok: potential-dos-via-decompression-bomb + _, err := io.CopyN(os.Stdout, r, buf, 1024*1024*4) + if err != nil { + panic(err) + } + r.Close() +} diff --git a/go/lang/security/decompression_bomb.yaml b/go/lang/security/decompression_bomb.yaml new file mode 100644 index 00000000..295d81b5 --- /dev/null +++ b/go/lang/security/decompression_bomb.yaml @@ -0,0 +1,62 @@ +rules: +- id: potential-dos-via-decompression-bomb + message: >- + Detected a possible denial-of-service via a zip bomb attack. By limiting the max + bytes read, you can mitigate this attack. + `io.CopyN()` can specify a size. + severity: WARNING + languages: [go] + patterns: + - pattern-either: + - pattern: io.Copy(...) + - pattern: io.CopyBuffer(...) + - pattern-either: + - pattern-inside: | + gzip.NewReader(...) + ... + - pattern-inside: | + zlib.NewReader(...) + ... + - pattern-inside: | + zlib.NewReaderDict(...) + ... + - pattern-inside: | + bzip2.NewReader(...) + ... + - pattern-inside: | + flate.NewReader(...) + ... + - pattern-inside: | + flate.NewReaderDict(...) + ... + - pattern-inside: | + lzw.NewReader(...) + ... + - pattern-inside: | + tar.NewReader(...) + ... + - pattern-inside: | + zip.NewReader(...) + ... + - pattern-inside: | + zip.OpenReader(...) + ... + fix-regex: + regex: (.*)(Copy|CopyBuffer)\((.*?),(.*?)(\)|,.*\)) + replacement: \1CopyN(\3, \4, 1024*1024*256) + metadata: + cwe: + - 'CWE-400: Uncontrolled Resource Consumption' + source-rule-url: https://github.com/securego/gosec + references: + - https://golang.org/pkg/io/#CopyN + - https://github.com/securego/gosec/blob/master/rules/decompression-bomb.go + category: security + technology: + - go + confidence: LOW + cwe2022-top25: true + subcategory: + - audit + likelihood: LOW + impact: MEDIUM diff --git a/go/lang/security/filepath-clean-misuse.fixed.go b/go/lang/security/filepath-clean-misuse.fixed.go new file mode 100644 index 00000000..b36e5e1d --- /dev/null +++ b/go/lang/security/filepath-clean-misuse.fixed.go @@ -0,0 +1,103 @@ +package main + +import ( + "io/ioutil" + "log" + "net/http" + "path/filepath" + "path" + "strings" +) + +const root = "/tmp" + +func main() { + mux := http.NewServeMux() + mux.HandleFunc("/bad1", func(w http.ResponseWriter, r *http.Request) { + // ruleid: filepath-clean-misuse + filename := filepath.FromSlash(filepath.Clean("/"+strings.Trim(r.URL.Path, "/"))) + filename := filepath.Join(root, strings.Trim(filename, "/")) + contents, err := ioutil.ReadFile(filename) + if err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + w.Write(contents) + }) + + mux.HandleFunc("/bad2", func(w http.ResponseWriter, r *http.Request) { + // ruleid: filepath-clean-misuse + filename := filepath.FromSlash(filepath.Clean("/"+strings.Trim(r.URL.Path, "/"))) + filename := filepath.Join(root, strings.Trim(filename, "/")) + contents, err := ioutil.ReadFile(filename) + if err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + w.Write(contents) + }) + + mux.HandleFunc("/ok", func(w http.ResponseWriter, r *http.Request) { + filename := r.URL.Path + // ok: filepath-clean-misuse + filename := filepath.Join(root, strings.Trim(filename, "/")) + contents, err := ioutil.ReadFile(filename) + if err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + w.Write(contents) + }) + + mux.HandleFunc("/ok2", func(w http.ResponseWriter, r *http.Request) { + // ok: filepath-clean-misuse + filename := path.Clean("/" + r.URL.Path) + filename := filepath.Join(root, strings.Trim(filename, "/")) + contents, err := ioutil.ReadFile(filename) + if err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + w.Write(contents) + }) + + server := &http.Server{ + Addr: "127.0.0.1:50000", + Handler: mux, + } + + log.Fatal(server.ListenAndServe()) +} + +// TODO +// func NewHandlerWithDefault(root http.FileSystem, handler http.Handler, defaultPath string, gatewayDomains []string) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// if isGatewayRequest(r) { +// // s3 signed request reaching the ui handler, return an error response instead of the default path +// o := operations.Operation{} +// err := errors.Codes[errors.ERRLakeFSWrongEndpoint] +// err.Description = fmt.Sprintf("%s (%v)", err.Description, gatewayDomains) +// o.EncodeError(w, r, err) +// return +// } +// urlPath := r.URL.Path +// // We want this rule to only fire when urlPath does not have +// // a slash in front. This if condition ensures there is a slash, +// // so the line marked 'ok' below should not fire. +// if !strings.HasPrefix(urlPath, "/") { +// urlPath = "/" + urlPath +// r.URL.Path = urlPath +// } +// // ok: filepath-clean-misuse +// _, err := root.Open(path.Clean(urlPath)) +// if err != nil && os.IsNotExist(err) { +// r.URL.Path = defaultPath +// } +// // consistent content-type +// contentType := gomime.TypeByExtension(filepath.Ext(r.URL.Path)) +// if contentType != "" { +// w.Header().Set("Content-Type", contentType) +// } +// handler.ServeHTTP(w, r) +// }) +// } \ No newline at end of file diff --git a/go/lang/security/filepath-clean-misuse.go b/go/lang/security/filepath-clean-misuse.go new file mode 100644 index 00000000..52c7611d --- /dev/null +++ b/go/lang/security/filepath-clean-misuse.go @@ -0,0 +1,103 @@ +package main + +import ( + "io/ioutil" + "log" + "net/http" + "path/filepath" + "path" + "strings" +) + +const root = "/tmp" + +func main() { + mux := http.NewServeMux() + mux.HandleFunc("/bad1", func(w http.ResponseWriter, r *http.Request) { + // ruleid: filepath-clean-misuse + filename := filepath.Clean(r.URL.Path) + filename := filepath.Join(root, strings.Trim(filename, "/")) + contents, err := ioutil.ReadFile(filename) + if err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + w.Write(contents) + }) + + mux.HandleFunc("/bad2", func(w http.ResponseWriter, r *http.Request) { + // ruleid: filepath-clean-misuse + filename := path.Clean(r.URL.Path) + filename := filepath.Join(root, strings.Trim(filename, "/")) + contents, err := ioutil.ReadFile(filename) + if err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + w.Write(contents) + }) + + mux.HandleFunc("/ok", func(w http.ResponseWriter, r *http.Request) { + filename := r.URL.Path + // ok: filepath-clean-misuse + filename := filepath.Join(root, strings.Trim(filename, "/")) + contents, err := ioutil.ReadFile(filename) + if err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + w.Write(contents) + }) + + mux.HandleFunc("/ok2", func(w http.ResponseWriter, r *http.Request) { + // ok: filepath-clean-misuse + filename := path.Clean("/" + r.URL.Path) + filename := filepath.Join(root, strings.Trim(filename, "/")) + contents, err := ioutil.ReadFile(filename) + if err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + w.Write(contents) + }) + + server := &http.Server{ + Addr: "127.0.0.1:50000", + Handler: mux, + } + + log.Fatal(server.ListenAndServe()) +} + +// TODO +// func NewHandlerWithDefault(root http.FileSystem, handler http.Handler, defaultPath string, gatewayDomains []string) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// if isGatewayRequest(r) { +// // s3 signed request reaching the ui handler, return an error response instead of the default path +// o := operations.Operation{} +// err := errors.Codes[errors.ERRLakeFSWrongEndpoint] +// err.Description = fmt.Sprintf("%s (%v)", err.Description, gatewayDomains) +// o.EncodeError(w, r, err) +// return +// } +// urlPath := r.URL.Path +// // We want this rule to only fire when urlPath does not have +// // a slash in front. This if condition ensures there is a slash, +// // so the line marked 'ok' below should not fire. +// if !strings.HasPrefix(urlPath, "/") { +// urlPath = "/" + urlPath +// r.URL.Path = urlPath +// } +// // ok: filepath-clean-misuse +// _, err := root.Open(path.Clean(urlPath)) +// if err != nil && os.IsNotExist(err) { +// r.URL.Path = defaultPath +// } +// // consistent content-type +// contentType := gomime.TypeByExtension(filepath.Ext(r.URL.Path)) +// if contentType != "" { +// w.Header().Set("Content-Type", contentType) +// } +// handler.ServeHTTP(w, r) +// }) +// } \ No newline at end of file diff --git a/go/lang/security/filepath-clean-misuse.yaml b/go/lang/security/filepath-clean-misuse.yaml new file mode 100644 index 00000000..516b6d85 --- /dev/null +++ b/go/lang/security/filepath-clean-misuse.yaml @@ -0,0 +1,58 @@ +rules: +- id: filepath-clean-misuse + message: >- + `Clean` is not intended to sanitize against path traversal attacks. + This function is for finding the shortest path name equivalent to the given input. + Using `Clean` to sanitize file reads may expose this application to + path traversal attacks, where an attacker could access arbitrary files on the server. + To fix this easily, write this: `filepath.FromSlash(path.Clean("/"+strings.Trim(req.URL.Path, "/")))` + However, a better solution is using the `SecureJoin` function in the package `filepath-securejoin`. + See https://pkg.go.dev/github.com/cyphar/filepath-securejoin#section-readme. + severity: ERROR + languages: [go] + mode: taint + pattern-sources: + - patterns: + - pattern-either: + - pattern: | + ($REQUEST : *http.Request).$ANYTHING + - pattern: | + ($REQUEST : http.Request).$ANYTHING + - metavariable-regex: + metavariable: $ANYTHING + regex: ^(BasicAuth|Body|Cookie|Cookies|Form|FormValue|GetBody|Host|MultipartReader|ParseForm|ParseMultipartForm|PostForm|PostFormValue|Referer|RequestURI|Trailer|TransferEncoding|UserAgent|URL)$ + pattern-sinks: + - patterns: + - pattern-either: + - pattern: filepath.Clean($...INNER) + - pattern: path.Clean($...INNER) + pattern-sanitizers: + - pattern-either: + - pattern: | + "/" + ... + fix: filepath.FromSlash(filepath.Clean("/"+strings.Trim($...INNER, "/"))) + options: + interfile: true + metadata: + references: + - https://pkg.go.dev/path#Clean + - http://technosophos.com/2016/03/31/go-quickly-cleaning-filepaths.html + - https://labs.detectify.com/2021/12/15/zero-day-path-traversal-grafana/ + - https://dzx.cz/2021/04/02/go_path_traversal/ + - https://pkg.go.dev/github.com/cyphar/filepath-securejoin#section-readme + cwe: + - "CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')" + owasp: + - A05:2017 - Broken Access Control + - A01:2021 - Broken Access Control + category: security + technology: + - go + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - vuln + likelihood: MEDIUM + impact: MEDIUM + confidence: MEDIUM + interfile: true diff --git a/go/lang/security/injection/open-redirect.go b/go/lang/security/injection/open-redirect.go new file mode 100644 index 00000000..869099d7 --- /dev/null +++ b/go/lang/security/injection/open-redirect.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "net/http" + "strings" +) + +func newRedirectServerFmt(addr string, rootPath string) *http.Server { + return &http.Server{ + Addr: addr, + Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + target := fmt.Sprintf("https://%s/path/to/%s", req.Host, req.URL.Path) + if rootPath != "" { + target += "/" + strings.TrimRight(strings.TrimLeft(rootPath, "/"), "/") + } + target += req.URL.Path + if len(req.URL.RawQuery) > 0 { + target += "?" + req.URL.RawQuery + } + // ruleid: open-redirect + http.Redirect(w, req, target, http.StatusTemporaryRedirect) + }), + } +} + +func newRedirectServerAdd(addr string, rootPath string) *http.Server { + return &http.Server{ + Addr: addr, + Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + target := "https://" + req.Host + "/path/to/" + req.URL.Path + if rootPath != "" { + target += "/" + strings.TrimRight(strings.TrimLeft(rootPath, "/"), "/") + } + target += req.URL.Path + if len(req.URL.RawQuery) > 0 { + target += "?" + req.URL.RawQuery + } + // ruleid: open-redirect + http.Redirect(w, req, target, http.StatusTemporaryRedirect) + }), + } +} + +func main() { + newRedirectServerAdd("127.0.0.1:8080", "/test") + newRedirectServerFmt("127.0.0.1:8080", "/test") +} diff --git a/go/lang/security/injection/open-redirect.yaml b/go/lang/security/injection/open-redirect.yaml new file mode 100644 index 00000000..6bafe1ed --- /dev/null +++ b/go/lang/security/injection/open-redirect.yaml @@ -0,0 +1,58 @@ +rules: + - id: open-redirect + languages: [ go ] + severity: WARNING + message: An HTTP redirect was found to be crafted from user-input `$REQUEST`. + This can lead to open redirect vulnerabilities, potentially allowing attackers + to redirect users to malicious web sites. It is recommend where possible to + not allow user-input to craft the redirect URL. When user-input is necessary + to craft the request, it is recommended to follow OWASP best practices to + restrict the URL to domains in an allowlist. + options: + interfile: true + metadata: + cwe: + - "CWE-601: URL Redirection to Untrusted Site ('Open Redirect')" + references: + - https://knowledge-base.secureflag.com/vulnerabilities/unvalidated_redirects___forwards/open_redirect_go_lang.html + category: security + technology: + - go + confidence: HIGH + description: "An HTTP redirect was found to be crafted from user-input leading to an open redirect vulnerability" + subcategory: + - vuln + impact: MEDIUM + likelihood: MEDIUM + interfile: true + mode: taint + pattern-sources: + - label: INPUT + patterns: + - pattern-either: + - pattern: | + ($REQUEST : *http.Request).$ANYTHING + - pattern: | + ($REQUEST : http.Request).$ANYTHING + - metavariable-regex: + metavariable: $ANYTHING + regex: ^(BasicAuth|Body|Cookie|Cookies|Form|FormValue|GetBody|Host|MultipartReader|ParseForm|ParseMultipartForm|PostForm|PostFormValue|Referer|RequestURI|Trailer|TransferEncoding|UserAgent|URL)$ + - label: CLEAN + requires: INPUT + patterns: + - pattern-either: + - pattern: | + "$URLSTR" + $INPUT + - patterns: + - pattern-either: + - pattern: fmt.Fprintf($F, "$URLSTR", $INPUT, ...) + - pattern: fmt.Sprintf("$URLSTR", $INPUT, ...) + - pattern: fmt.Printf("$URLSTR", $INPUT, ...) + - metavariable-regex: + metavariable: $URLSTR + regex: .*//[a-zA-Z0-10]+\..* + pattern-sinks: + - requires: INPUT and not CLEAN + patterns: + - pattern: http.Redirect($W, $REQ, $URL, ...) + - focus-metavariable: $URL diff --git a/go/lang/security/injection/raw-html-format.go b/go/lang/security/injection/raw-html-format.go new file mode 100644 index 00000000..1e81b178 --- /dev/null +++ b/go/lang/security/injection/raw-html-format.go @@ -0,0 +1,129 @@ +package main + +import ( + "fmt" + "log" + "net/http" +) + +func getMovieQuote() map[string]string { + m := make(map[string]string) + m["quote"] = "I'll be back." + m["movie"] = "The Terminator" + m["year"] = "1984" + + return m +} + +func healthCheck(w http.ResponseWriter, r *http.Request) { + // ok: raw-html-format + w.Write([]byte("alive")) +} + +func indexPage(w http.ResponseWriter, r *http.Request) { + const tme = `` + + const template = ` + + +

Random Movie Quotes

+

%s

+

~%s, %s

+ + ` + + quote := getMovieQuote() + + quoteText := quote["quote"] + movie := quote["movie"] + year := quote["year"] + + w.WriteHeader(http.StatusAccepted) + // ok: raw-html-format + w.Write([]byte(fmt.Sprintf(template, quoteText, movie, year))) +} + +func errorPage(w http.ResponseWriter, r *http.Request) { + params := r.URL.Query() + urls, ok := params["url"] + if !ok { + log.Println("Error") + return + } + url := urls[0] + + const template = ` + + +

error; page not found. go back

+ + ` + + w.WriteHeader(http.StatusAccepted) + // ruleid:raw-html-format + w.Write([]byte(fmt.Sprintf(template, url))) +} + +func errorPage2(w http.ResponseWriter, r *http.Request) { + params := r.URL.Query() + urls, ok := params["url"] + if !ok { + log.Println("Error") + return + } + url := urls[0] + + const template = ` + + +

error; page not found. go back

+ + ` + + w.WriteHeader(http.StatusAccepted) + // ruleid:raw-html-format + w.Write([]byte(fmt.Printf(template, url))) +} + +func errorPage3(w http.ResponseWriter, r *http.Request) { + params := r.URL.Query() + urls, ok := params["url"] + if !ok { + log.Println("Error") + return + } + url := urls[0] + + const template = ` + + +

error; page not found. go back

+ + ` + + w.WriteHeader(http.StatusAccepted) + // ruleid:raw-html-format + fmt.Fprintf(w, template, url) +} + +func errorPage4(w http.ResponseWriter, r *http.Request) { + params := r.URL.Query() + urls, ok := params["url"] + if !ok { + log.Println("Error") + return + } + url := urls[0] + + // ruleid:raw-html-format + const template = "

error; page not found. go back

" + + w.WriteHeader(http.StatusAccepted) + w.Write([]byte(template)) +} + +func main() { + http.HandleFunc("/", indexPage) + http.HandleFunc("/error", errorPage) + http.ListenAndServe(":8080", nil) +} diff --git a/go/lang/security/injection/raw-html-format.yaml b/go/lang/security/injection/raw-html-format.yaml new file mode 100644 index 00000000..267b2789 --- /dev/null +++ b/go/lang/security/injection/raw-html-format.yaml @@ -0,0 +1,54 @@ +rules: +- id: raw-html-format + languages: [go] + severity: WARNING + message: >- + Detected user input flowing into a manually constructed HTML string. You may be + accidentally bypassing secure methods + of rendering HTML by manually constructing HTML and this could create a cross-site + scripting vulnerability, which could + let attackers steal sensitive user data. Use the `html/template` package which + will safely render HTML instead, or inspect + that the HTML is rendered safely. + metadata: + cwe: + - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')" + owasp: + - A07:2017 - Cross-Site Scripting (XSS) + - A03:2021 - Injection + category: security + technology: + - go + references: + - https://blogtitle.github.io/robn-go-security-pearls-cross-site-scripting-xss/ + confidence: MEDIUM + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - vuln + likelihood: HIGH + impact: MEDIUM + mode: taint + pattern-sources: + - patterns: + - pattern-either: + - pattern: | + ($REQUEST : *http.Request).$ANYTHING + - pattern: | + ($REQUEST : http.Request).$ANYTHING + - metavariable-regex: + metavariable: $ANYTHING + regex: ^(BasicAuth|Body|Cookie|Cookies|Form|FormValue|GetBody|Host|MultipartReader|ParseForm|ParseMultipartForm|PostForm|PostFormValue|Referer|RequestURI|Trailer|TransferEncoding|UserAgent|URL)$ + pattern-sanitizers: + - pattern: html.EscapeString(...) + pattern-sinks: + - patterns: + - pattern-either: + - pattern: fmt.Printf("$HTMLSTR", ...) + - pattern: fmt.Sprintf("$HTMLSTR", ...) + - pattern: fmt.Fprintf($W, "$HTMLSTR", ...) + - pattern: '"$HTMLSTR" + ...' + - metavariable-pattern: + metavariable: $HTMLSTR + language: generic + pattern: <$TAG ... diff --git a/go/lang/security/injection/tainted-sql-string.go b/go/lang/security/injection/tainted-sql-string.go new file mode 100644 index 00000000..8c4888f9 --- /dev/null +++ b/go/lang/security/injection/tainted-sql-string.go @@ -0,0 +1,145 @@ +package main + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "database/sql" +) + +func DeleteHandler(db *sql.DB) func(w http.ResponseWriter, req *http.Request) { + return func(w http.ResponseWriter, req *http.Request) { + del := req.URL.Query().Get("del") + id := req.URL.Query().Get("Id") + if del == "del" { + // ruleid: tainted-sql-string + _, err = db.Exec("DELETE FROM table WHERE Id = " + id) + if err != nil { + panic(err) + } + } + } +} + +func DeleteHandlerOk(db *sql.DB) func(w http.ResponseWriter, req *http.Request) { + return func(w http.ResponseWriter, req *http.Request) { + del := req.URL.Query().Get("del") + idhtml := req.URL.Query().Get("Id") + + id, _ := strconv.Atoi(idhtml) + + if del == "del" { + // ok: tainted-sql-string + _, err = db.Exec("DELETE FROM table WHERE Id = " + id) + if err != nil { + panic(err) + } + } + } +} + +func SelectHandler(db *sql.DB) func(w http.ResponseWriter, req *http.Request) { + return func(w http.ResponseWriter, req *http.Request) { + del := req.URL.Query().Get("del") + id := req.URL.Query().Get("Id") + if del == "del" { + // ruleid: tainted-sql-string + sql := fmt.Sprintf("SELECT * FROM table WHERE Id = %v", id) + _, err = db.Exec(sql) + if err != nil { + panic(err) + } + } + } +} + +func SelectHandler2(db *sql.DB) func(w http.ResponseWriter, req *http.Request) { + return func(w http.ResponseWriter, req *http.Request) { + var sb strings.Builder + del := req.URL.Query().Get("del") + id := req.URL.Query().Get("Id") + if del == "del" { + sb.WriteString("SELECT * FROM table WHERE Id = ") + // ruleid: tainted-sql-string + sb.WriteString(id) + + sql := sb.String() + _, err = db.Exec(sql) + if err != nil { + panic(err) + } + } + } +} + +func SelectHandler2ok(db *sql.DB) func(w http.ResponseWriter, req *http.Request) { + return func(w http.ResponseWriter, req *http.Request) { + var sb strings.Builder + del := req.URL.Query().Get("del") + id := req.URL.Query().Get("Id") + if del == "del" { + sb.WriteString("SELECT * FROM table WHERE Id = ") + // ok: tainted-sql-string + sb.WriteString(id) + + sql := "select hello" + _, err = db.Exec(sql) + if err != nil { + panic(err) + } + } + } +} + +func SelectHandler3(db *sql.DB) func(w http.ResponseWriter, req *http.Request) { + return func(w http.ResponseWriter, req *http.Request) { + del := req.URL.Query().Get("del") + id := req.URL.Query().Get("Id") + if del == "del" { + sql := "SELECT * FROM table WHERE Id = " + // ruleid: tainted-sql-string + sql += id + _, err = db.Exec(sql) + if err != nil { + panic(err) + } + } + } +} + +func SelectHandler3(db *sql.DB) func(w http.ResponseWriter, req *http.Request) { + return func(w http.ResponseWriter, req *http.Request) { + del := req.URL.Query().Get("del") + id := req.URL.Query().Get("Id") + if del == "del" { + sql := "SELECT * FROM table WHERE Id = " + // ok: tainted-sql-string + sql += (id != 3) + _, err = db.Exec(sql) + if err != nil { + panic(err) + } + } + } +} + +func SelectHandlerOk(db *sql.DB) func(w http.ResponseWriter, req *http.Request) { + return func(w http.ResponseWriter, req *http.Request) { + del := req.URL.Query().Get("del") + id := req.URL.Query().Get("Id") + + if del == "del" { + // ok: tainted-sql-string + _, err = db.QueryRow("SELECT * FROM table WHERE Id = $1", id) + + // ok: tainted-sql-string + fmt.Fprintf(w, "Deleted %s", id) + if err != nil { + panic(err) + } + } + } +} \ No newline at end of file diff --git a/go/lang/security/injection/tainted-sql-string.yaml b/go/lang/security/injection/tainted-sql-string.yaml new file mode 100644 index 00000000..78f4a3f6 --- /dev/null +++ b/go/lang/security/injection/tainted-sql-string.yaml @@ -0,0 +1,83 @@ +rules: +- id: tainted-sql-string + languages: [go] + message: >- + User data flows into this manually-constructed SQL string. User data + can be safely inserted into SQL strings using prepared statements or an + object-relational mapper (ORM). Manually-constructed SQL strings is a + possible indicator of SQL injection, which could let an attacker steal + or manipulate data from the database. + Instead, use prepared statements (`db.Query("SELECT * FROM t WHERE id = ?", id)`) + or a safe library. + options: + interfile: true + metadata: + cwe: + - "CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')" + owasp: + - A01:2017 - Injection + - A03:2021 - Injection + references: + - https://golang.org/doc/database/sql-injection + - https://www.stackhawk.com/blog/golang-sql-injection-guide-examples-and-prevention/ + category: security + technology: + - go + confidence: HIGH + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - vuln + likelihood: HIGH + impact: MEDIUM + interfile: true + mode: taint + severity: ERROR + pattern-sources: + - patterns: + - pattern-either: + - pattern: | + ($REQUEST : *http.Request).$ANYTHING + - pattern: | + ($REQUEST : http.Request).$ANYTHING + - metavariable-regex: + metavariable: $ANYTHING + regex: ^(BasicAuth|Body|Cookie|Cookies|Form|FormValue|GetBody|Host|MultipartReader|ParseForm|ParseMultipartForm|PostForm|PostFormValue|Referer|RequestURI|Trailer|TransferEncoding|UserAgent|URL)$ + pattern-sinks: + - patterns: + - pattern-either: + - patterns: + - pattern-either: + - pattern: | + "$SQLSTR" + ... + - patterns: + - pattern-inside: | + $VAR = "$SQLSTR"; + ... + - pattern: $VAR += ... + - patterns: + - pattern-inside: | + var $SB strings.Builder + ... + - pattern-inside: | + $SB.WriteString("$SQLSTR") + ... + $SB.String(...) + - pattern: | + $SB.WriteString(...) + - metavariable-regex: + metavariable: $SQLSTR + regex: (?i)(select|delete|insert|create|update|alter|drop).* + - patterns: + - pattern-either: + - pattern: fmt.Fprintf($F, "$SQLSTR", ...) + - pattern: fmt.Sprintf("$SQLSTR", ...) + - pattern: fmt.Printf("$SQLSTR", ...) + - metavariable-regex: + metavariable: $SQLSTR + regex: \s*(?i)(select|delete|insert|create|update|alter|drop)\b.*%(v|s|q).* + pattern-sanitizers: + - pattern-either: + - pattern: strconv.Atoi(...) + - pattern: | + ($X: bool) diff --git a/go/lang/security/injection/tainted-url-host.go b/go/lang/security/injection/tainted-url-host.go new file mode 100644 index 00000000..079a796b --- /dev/null +++ b/go/lang/security/injection/tainted-url-host.go @@ -0,0 +1,387 @@ +package main + +import ( + "crypto/tls" + "encoding/hex" + "fmt" + "io/ioutil" + "net/http" +) + +func handlerIndexFmt(w http.ResponseWriter, r *http.Request) { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + client := &http.Client{Transport: tr} + + if r.Method == "POST" && r.URL.Path == "/api" { + url := fmt.Sprintf("https://%v/api", r.URL.Query().Get("proxy")) + + // ruleid: tainted-url-host + resp, err := client.Post(url, "application/json", r.Body) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + defer resp.Body.Close() + + if resp.StatusCode != 200 { + w.WriteHeader(500) + return + } + + w.Write([]byte(fmt.Sprintf("{\"host\":\"%v\"}", r.URL.Query().Get("proxy")))) + return + } else { + proxy := r.URL.Query()["proxy"] + secure := r.URL.Query()["secure"] + + url := "" + if secure { + url = fmt.Sprintf("https://%s", proxy) + } else { + url = fmt.Sprintf("http://%q", proxy) + } + // ruleid: tainted-url-host + resp, err := client.Post(url, "application/json", r.Body) + } +} + +func handlerOtherFmt(w http.ResponseWriter, r *http.Request) { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + client := &http.Client{Transport: tr} + + if r.Method == "POST" && r.URL.Path == "/api" { + url := fmt.Printf("https://%v/api", r.URL.Query().Get("proxy")) + + // ruleid: tainted-url-host + resp, err := client.Post(url, "application/json", r.Body) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + defer resp.Body.Close() + + if resp.StatusCode != 200 { + w.WriteHeader(500) + return + } + + w.Write([]byte(fmt.Sprintf("{\"host\":\"%v\"}", r.URL.Query().Get("proxy")))) + return + } else { + proxy := r.URL.Query()["proxy"] + secure := r.URL.Query()["secure"] + + url := "" + if secure { + url = fmt.Fprintf(w, "https://%s", proxy) + } else { + url = fmt.Fprintf(w, "http://%q", proxy) + } + // ruleid: tainted-url-host + resp, err := client.Post(url, "application/json", r.Body) + } +} + +func handlerOkFmt(w http.ResponseWriter, r *http.Request) { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + client := &http.Client{Transport: tr} + + if r.Method == "POST" && r.URL.Path == "/api" { + url := fmt.Printf("https://example.com/%v", r.URL.Query().Get("proxy")) + + // ok: tainted-url-host + resp, err := client.Post(url, "application/json", r.Body) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + defer resp.Body.Close() + + if resp.StatusCode != 200 { + w.WriteHeader(500) + return + } + + w.Write([]byte(fmt.Sprintf("{\"host\":\"%v\"}", r.URL.Query().Get("proxy")))) + return + } else { + proxy := r.URL.Query()["proxy"] + secure := r.URL.Query()["secure"] + + url := "" + if secure { + url = fmt.Sprintf("https://example.com/%s", proxy) + } else { + url = fmt.Fprintf(w, "http://example.com%q", proxy) + } + // ok: tainted-url-host + resp, err := client.Post(url, "application/json", r.Body) + } +} + +func (s *server) handlerBadFmt(w http.ResponseWriter, r *http.Request) { + urls, ok := r.URL.Query()["url"] // extract url from query params + + if !ok { + http.Error(w, "url missing", 500) + return + } + + if len(urls) != 1 { + http.Error(w, "url missing", 500) + return + } + + url := fmt.Sprintf("//%s/path", urls[0]) + + // ruleid: tainted-url-host + resp, err := http.Get(url) // sink + if err != nil { + http.Error(w, err.Error(), 500) + return + } + + client := &http.Client{} + + // ruleid: tainted-url-host + req2, err := http.NewRequest("GET", url, nil) + _, err2 := client.Do(req2) + if err2 != nil { + http.Error(w, err.Error(), 500) + return + } + + // ok: tainted-url-host + _, err3 := http.Get("https://semgrep.dev") + if err3 != nil { + http.Error(w, err.Error(), 500) + return + } + + url4 := fmt.Sprintf("ftps://%s/path/to/%s", "test", r.URL.Path) + // ok: tainted-url-host + _, err4 := http.Get("https://semgrep.dev") + if err3 != nil { + http.Error(w, err.Error(), 500) + return + } + + defer resp.Body.Close() + + bytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + + // Write out the hexdump of the bytes as plaintext. + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + fmt.Fprint(w, hex.Dump(bytes)) +} + +func handlerIndexAdd(w http.ResponseWriter, r *http.Request) { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + client := &http.Client{Transport: tr} + + if r.Method == "POST" && r.URL.Path == "/api" { + url := "https://" + r.URL.Query().Get("proxy") + "/api" + + // ruleid: tainted-url-host + resp, err := client.Post(url, "application/json", r.Body) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + defer resp.Body.Close() + + if resp.StatusCode != 200 { + w.WriteHeader(500) + return + } + + w.Write([]byte(fmt.Sprintf("{\"host\":\"%v\"}", r.URL.Query().Get("proxy")))) + return + } else { + proxy := r.URL.Query()["proxy"] + secure := r.URL.Query()["secure"] + + url := "" + if secure { + url = "https://" + proxy + } else { + url = "http://" + proxy + } + // ruleid: tainted-url-host + resp, err := client.Post(url, "application/json", r.Body) + } +} + +func handlerOtherAdd(w http.ResponseWriter, r *http.Request) { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + client := &http.Client{Transport: tr} + + if r.Method == "POST" && r.URL.Path == "/api" { + url := "https://" + r.URL.Query().Get("proxy") + "/api" + + // ruleid: tainted-url-host + resp, err := client.Post(url, "application/json", r.Body) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + defer resp.Body.Close() + + if resp.StatusCode != 200 { + w.WriteHeader(500) + return + } + + w.Write([]byte(fmt.Sprintf("{\"host\":\"%v\"}", r.URL.Query().Get("proxy")))) + return + } else { + proxy := r.URL.Query()["proxy"] + secure := r.URL.Query()["secure"] + + url := "" + if secure { + url = "https://example.com/" + proxy + } else { + url = "http://example.com/api/test/" + proxy + } + // ok: tainted-url-host + resp, err := client.Post(url, "application/json", r.Body) + } +} + +func handlerOkAdd(w http.ResponseWriter, r *http.Request) { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + client := &http.Client{Transport: tr} + + if r.Method == "POST" && r.URL.Path == "/api" { + // ok: tainted-url-host + resp, err := client.Post("https://example.com/"+r.URL.Query().Get("proxy"), "application/json", r.Body) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + defer resp.Body.Close() + + if resp.StatusCode != 200 { + w.WriteHeader(500) + return + } + + w.Write([]byte(fmt.Sprintf("{\"host\":\"%v\"}", r.URL.Query().Get("proxy")))) + return + } else { + proxy := r.URL.Query()["proxy"] + secure := r.URL.Query()["secure"] + + url := "" + if secure { + url = "https://example.com/" + proxy + } else { + url = "http://example.com" + proxy + } + // ok: tainted-url-host + resp, err := client.Post(url, "application/json", r.Body) + } +} + +func (s *server) handlerBadAdd(w http.ResponseWriter, r *http.Request) { + urls, ok := r.URL.Query()["url"] // extract url from query params + + if !ok { + http.Error(w, "url missing", 500) + return + } + + if len(urls) != 1 { + http.Error(w, "url missing", 500) + return + } + + url := urls[0] + + // ruleid: tainted-url-host + resp, err := http.Get(url) // sink + if err != nil { + http.Error(w, err.Error(), 500) + return + } + + client := &http.Client{} + + // ruleid: tainted-url-host + req2, err := http.NewRequest("GET", r.URL.Path, nil) + _, err2 := client.Do(req2) + if err2 != nil { + http.Error(w, err.Error(), 500) + return + } + + // ok: tainted-url-host + _, err3 := http.Get("https://semgrep.dev") + if err3 != nil { + http.Error(w, err.Error(), 500) + return + } + + url4 := fmt.Sprintf("ftps://%s/path/to/%s", "test", r.URL.Path) + // ok: tainted-url-host + _, err4 := http.Get("https://semgrep.dev") + if err3 != nil { + http.Error(w, err.Error(), 500) + return + } + + defer resp.Body.Close() + + bytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + + // Write out the hexdump of the bytes as plaintext. + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + fmt.Fprint(w, hex.Dump(bytes)) +} + +func main() { + http.HandleFunc("/", handlerIndex) + http.HandleFunc("/other", handleOther) + http.HandleFunc("/ok", handleOk) + http.HandleFunc("/bad", handlerBad) + http.ListenAndServe(":8888", nil) +} diff --git a/go/lang/security/injection/tainted-url-host.yaml b/go/lang/security/injection/tainted-url-host.yaml new file mode 100644 index 00000000..999fcaea --- /dev/null +++ b/go/lang/security/injection/tainted-url-host.yaml @@ -0,0 +1,80 @@ +rules: + - id: tainted-url-host + languages: + - go + message: A request was found to be crafted from user-input `$REQUEST`. This can + lead to Server-Side Request Forgery (SSRF) vulnerabilities, potentially + exposing sensitive data. It is recommend where possible to not allow + user-input to craft the base request, but to be treated as part of the + path or query parameter. When user-input is necessary to craft the + request, it is recommended to follow OWASP best practices to prevent + abuse, including using an allowlist. + options: + interfile: true + metadata: + cwe: + - "CWE-918: Server-Side Request Forgery (SSRF)" + owasp: + - A10:2021 - Server-Side Request Forgery (SSRF) + references: + - https://goteleport.com/blog/ssrf-attacks/ + category: security + technology: + - go + confidence: HIGH + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - vuln + impact: MEDIUM + likelihood: MEDIUM + interfile: true + mode: taint + pattern-sources: + - label: INPUT + patterns: + - pattern-either: + - pattern: | + ($REQUEST : *http.Request).$ANYTHING + - pattern: | + ($REQUEST : http.Request).$ANYTHING + - metavariable-regex: + metavariable: $ANYTHING + regex: ^(BasicAuth|Body|Cookie|Cookies|Form|FormValue|GetBody|Host|MultipartReader|ParseForm|ParseMultipartForm|PostForm|PostFormValue|Referer|RequestURI|Trailer|TransferEncoding|UserAgent|URL)$ + - label: CLEAN + requires: INPUT + patterns: + - pattern-either: + - pattern: | + "$URLSTR" + $INPUT + - patterns: + - pattern-either: + - pattern: fmt.Fprintf($F, "$URLSTR", $INPUT, ...) + - pattern: fmt.Sprintf("$URLSTR", $INPUT, ...) + - pattern: fmt.Printf("$URLSTR", $INPUT, ...) + - metavariable-regex: + metavariable: $URLSTR + regex: .*//[a-zA-Z0-10]+\..* + pattern-sinks: + - requires: INPUT and not CLEAN + patterns: + - pattern-either: + - patterns: + - pattern-either: + - patterns: + - pattern-inside: | + $CLIENT := &http.Client{...} + ... + - pattern: $CLIENT.$METHOD($URL, ...) + - pattern: http.$METHOD($URL, ...) + - metavariable-regex: + metavariable: $METHOD + regex: ^(Get|Head|Post|PostForm)$ + - patterns: + - pattern: | + http.NewRequest("$METHOD", $URL, ...) + - metavariable-regex: + metavariable: $METHOD + regex: ^(GET|HEAD|POST|POSTFORM)$ + - focus-metavariable: $URL + severity: WARNING \ No newline at end of file diff --git a/go/lang/security/reverseproxy-director.go b/go/lang/security/reverseproxy-director.go new file mode 100644 index 00000000..0a7b1762 --- /dev/null +++ b/go/lang/security/reverseproxy-director.go @@ -0,0 +1,65 @@ +package main + +import ( + "log" + "net/http" + "net/http/httputil" + "net/url" +) + +func NewProxy(targetHost string) (*httputil.ReverseProxy, error) { + url, err := url.Parse(targetHost) + if err != nil { + return nil, err + } + + proxy := httputil.NewSingleHostReverseProxy(url) + + originalDirector := proxy.Director + // ruleid: reverseproxy-director + proxy.Director = func(req *http.Request) { + originalDirector(req) + modifyRequest(req) + } + return proxy, nil +} + +func modifyRequest(req *http.Request) { + req.Header.Set("Extra-Header", "nice") +} + +func ProxyRequestHandler(proxy *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + proxy.ServeHTTP(w, r) + } +} + +type Fake struct { + Director string +} + +func extraCases() { + rp := &httputil.ReverseProxy{ + // ruleid: reverseproxy-director + Director: func(req *http.Request) { + modifyRequest(req) + }, + } + _ = rp + + f := Fake{ + // ok: reverseproxy-director + Director: "abcd", + } + _ = f +} + +func main() { + proxy, err := NewProxy("https://example.com") + if err != nil { + panic(err) + } + + http.HandleFunc("/", ProxyRequestHandler(proxy)) + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/go/lang/security/reverseproxy-director.yaml b/go/lang/security/reverseproxy-director.yaml new file mode 100644 index 00000000..ba210b6c --- /dev/null +++ b/go/lang/security/reverseproxy-director.yaml @@ -0,0 +1,33 @@ +rules: +- id: reverseproxy-director + message: >- + ReverseProxy can remove headers added by Director. Consider using ReverseProxy.Rewrite + instead of ReverseProxy.Director. + languages: [go] + severity: WARNING + patterns: + - pattern-inside: | + import "net/http/httputil" + ... + - pattern-either: + - pattern: $PROXY.Director = $FUNC + - patterns: + - pattern-inside: | + httputil.ReverseProxy{ + ... + } + - pattern: | + Director: $FUNC + metadata: + cwe: + - "CWE-115: Misinterpretation of Input" + category: security + subcategory: + - audit + technology: + - go + confidence: MEDIUM + likelihood: LOW + impact: LOW + references: + - https://github.com/golang/go/issues/50580 diff --git a/go/lang/security/shared-url-struct-mutation.go b/go/lang/security/shared-url-struct-mutation.go new file mode 100644 index 00000000..005d2007 --- /dev/null +++ b/go/lang/security/shared-url-struct-mutation.go @@ -0,0 +1,118 @@ +package main + +import ( + "net/http" + "net/url" +) + +var redirectURL, _ = url.Parse("https://example.com") + +func getRedirectToken() (string, error) { + return "abcd", nil +} + +func handler1(w http.ResponseWriter, r *http.Request) { + u := redirectURL + q := u.Query() + + // opaque process that might fail + token, err := getRedirectToken() + if err != nil { + q.Set("error", err.Error()) + } else { + q.Set("token", token) + } + // ruleid: shared-url-struct-mutation + u.RawQuery = q.Encode() + r.URL.RawQuery = q.Encode() + + http.Redirect(w, r, u.String(), http.StatusFound) +} + +func handler2(w http.ResponseWriter, r *http.Request) { + u, _ := url.Parse("https://example.com") + + q := u.Query() + + // opaque process that might fail + token, err := getRedirectToken() + if err != nil { + q.Set("error", err.Error()) + } else { + q.Set("token", token) + } + // ok: shared-url-struct-mutation + u.RawQuery = q.Encode() + + http.Redirect(w, r, u.String(), http.StatusFound) +} + +func handler3(w http.ResponseWriter, r *http.Request) { + u := url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/", + } + q := u.Query() + + // opaque process that might fail + token, err := getRedirectToken() + if err != nil { + q.Set("error", err.Error()) + } else { + q.Set("token", token) + } + + u.RawQuery = q.Encode() + + http.Redirect(w, r, u.String(), http.StatusFound) +} + +func handler4(w http.ResponseWriter, r *http.Request) { + var u *url.URL + if true { + u, _ = url.Parse("https://example.com") + } + + if u != nil { + + q := u.Query() + + // opaque process that might fail + token, err := getRedirectToken() + if err != nil { + q.Set("error", err.Error()) + } else { + q.Set("token", token) + } + // ok: shared-url-struct-mutation + u.RawQuery = q.Encode() + + http.Redirect(w, r, u.String(), http.StatusFound) + } + http.Redirect(w, r, "https://google.com", http.StatusFound) +} + +func extraCases(w http.ResponseWriter, r *http.Request) { + var x struct { + y []struct { + Path string + } + } + // ok: shared-url-struct-mutation + r.URL.RawQuery = "abcd" + // ok: shared-url-struct-mutation + x.y[0].Path = "abcd" + + a, _ := url.ParseRequestURI("https://example.com") + // ok: shared-url-struct-mutation + a.RawQuery = "abcd" +} + +func main() { + http.HandleFunc("/1", handler1) + http.HandleFunc("/2", handler2) + http.HandleFunc("/3", handler3) + http.HandleFunc("/4", handler4) + http.ListenAndServe(":7777", nil) +} diff --git a/go/lang/security/shared-url-struct-mutation.yaml b/go/lang/security/shared-url-struct-mutation.yaml new file mode 100644 index 00000000..0dcd483a --- /dev/null +++ b/go/lang/security/shared-url-struct-mutation.yaml @@ -0,0 +1,52 @@ +rules: +- id: shared-url-struct-mutation + message: >- + Shared URL struct may have been accidentally mutated. Ensure that + this behavior is intended. + languages: [go] + severity: WARNING + patterns: + - pattern-inside: | + import "net/url" + ... + - pattern-not-inside: | + ... = url.Parse(...) + ... + - pattern-not-inside: | + ... = url.ParseRequestURI(...) + ... + - pattern-not-inside: | + ... = url.URL{...} + ... + - pattern-not-inside: | + var $URL *$X.URL + ... + - pattern-either: + - pattern: $URL.RawQuery = ... + - pattern: $URL.Path = ... + - pattern: $URL.RawPath = ... + - pattern: $URL.Fragment = ... + - pattern: $URL.RawFragment = ... + - pattern: $URL.Scheme = ... + - pattern: $URL.Opaque = ... + - pattern: $URL.Host = ... + - pattern: $URL.User = ... + - metavariable-pattern: + metavariable: $URL + patterns: + - pattern-not: $X.$Y + - pattern-not: $X[...] + metadata: + cwe: + - "CWE-436: Interpretation Conflict" + category: security + subcategory: + - audit + technology: + - go + confidence: LOW + likelihood: LOW + impact: LOW + references: + - https://github.com/golang/go/issues/63777 + diff --git a/go/lang/security/zip.go b/go/lang/security/zip.go new file mode 100644 index 00000000..5f0e2e23 --- /dev/null +++ b/go/lang/security/zip.go @@ -0,0 +1,75 @@ +package unzip + +import ( + "archive/zip" + "fmt" + "io" + "log" + "os" + "path/filepath" +) + +func unzip(archive, target string) error { + // ruleid: path-traversal-inside-zip-extraction + reader, err := zip.OpenReader(archive) + if err != nil { + return err + } + + if err := os.MkdirAll(target, 0750); err != nil { + return err + } + + for _, file := range reader.File { + path := filepath.Join(target, file.Name) + if file.FileInfo().IsDir() { + os.MkdirAll(path, file.Mode()) // #nosec + continue + } + + fileReader, err := file.Open() + if err != nil { + return err + } + defer fileReader.Close() + + targetFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode()) + if err != nil { + return err + } + defer targetFile.Close() + + if _, err := io.Copy(targetFile, fileReader); err != nil { + return err + } + } + + return nil +} + +func unzip_good() { + // Open a zip archive for reading. + r, err := zip.OpenReader("testdata/readme.zip") + if err != nil { + log.Fatal(err) + } + defer r.Close() + // Iterate through the files in the archive, + // printing some of their contents. + for _, f := range r.File { + fmt.Printf("Contents of %s:\n", f.Name) + rc, err := f.Open() + if err != nil { + log.Fatal(err) + } + _, err = io.CopyN(os.Stdout, rc, 68) + if err != nil { + log.Fatal(err) + } + rc.Close() + fmt.Println() + } + // Output: + // Contents of README: + // This is the source code repository for the Go programming language. +} diff --git a/go/lang/security/zip.yaml b/go/lang/security/zip.yaml new file mode 100644 index 00000000..e03aa57b --- /dev/null +++ b/go/lang/security/zip.yaml @@ -0,0 +1,32 @@ +rules: +- id: path-traversal-inside-zip-extraction + message: File traversal when extracting zip archive + metadata: + cwe: + - "CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')" + source_rule_url: https://github.com/securego/gosec/issues/205 + category: security + technology: + - go + confidence: LOW + owasp: + - A05:2017 - Broken Access Control + - A01:2021 - Broken Access Control + references: + - https://owasp.org/Top10/A01_2021-Broken_Access_Control + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - audit + likelihood: LOW + impact: LOW + languages: [go] + severity: WARNING + pattern: | + reader, $ERR := zip.OpenReader($ARCHIVE) + ... + for _, $FILE := range reader.File { + ... + path := filepath.Join($TARGET, $FILE.Name) + ... + } diff --git a/go/otto/security/audit/dangerous-execution.go b/go/otto/security/audit/dangerous-execution.go new file mode 100644 index 00000000..c3ea4fbd --- /dev/null +++ b/go/otto/security/audit/dangerous-execution.go @@ -0,0 +1,28 @@ +package blah + +import ( + "net/http" + "github.com/robertkrimen/otto" +) + +func whyyyy(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + panic(err) + } + script := r.Form.Get("script") + + vm := otto.New() + + // ruleid: dangerous-execution + vm.Run(script) +} + +func main() { + vm := otto.New() + // ok: dangerous-execution + vm.Run(` + abc = 2 + 2; + console.log("The value of abc is " + abc); // 4 + `) +} diff --git a/go/otto/security/audit/dangerous-execution.yaml b/go/otto/security/audit/dangerous-execution.yaml new file mode 100644 index 00000000..9a1c8d35 --- /dev/null +++ b/go/otto/security/audit/dangerous-execution.yaml @@ -0,0 +1,33 @@ +rules: +- id: dangerous-execution + message: >- + Detected non-static script inside otto VM. Audit the input to 'VM.Run'. + If unverified user data can reach this call site, this is a code injection + vulnerability. A malicious actor can inject a malicious script to execute + arbitrary code. + metadata: + cwe: + - "CWE-94: Improper Control of Generation of Code ('Code Injection')" + owasp: + - A03:2021 - Injection + category: security + technology: + - otto + - vm + confidence: LOW + references: + - https://owasp.org/Top10/A03_2021-Injection + cwe2022-top25: true + subcategory: + - audit + likelihood: LOW + impact: HIGH + severity: ERROR + patterns: + - pattern-inside: | + $VM = otto.New(...) + ... + - pattern-not: $VM.Run("...", ...) + - pattern: $VM.Run(...) + languages: + - go diff --git a/go/template/security/insecure-types.go b/go/template/security/insecure-types.go new file mode 100644 index 00000000..96adee70 --- /dev/null +++ b/go/template/security/insecure-types.go @@ -0,0 +1,31 @@ +package main + +import "fmt" +import "html/template" + +func main() { + var g = "foo" + + // ruleid:go-insecure-templates + const a template.HTML = fmt.Sprintf("link") + // ruleid:go-insecure-templates + var b template.CSS = "a { text-decoration: underline; } " + + // ruleid:go-insecure-templates + var c template.HTMLAttr = fmt.Sprintf("herf=%q") + + // ruleid:go-insecure-templates + const d template.JS = "{foo: 'bar'}" + + // ruleid:go-insecure-templates + var e template.JSStr = "setTimeout('alert()')"; + + // ruleid:go-insecure-templates + var f template.Srcset = g; + + // ok:go-insecure-templates + tmpl, err := template.New("test").ParseFiles("file.txt") + + // other code + myTpl.Execute(w, a); +} diff --git a/go/template/security/insecure-types.yaml b/go/template/security/insecure-types.yaml new file mode 100644 index 00000000..3ea82462 --- /dev/null +++ b/go/template/security/insecure-types.yaml @@ -0,0 +1,37 @@ +rules: +- id: go-insecure-templates + patterns: + - pattern-inside: | + import "html/template" + ... + - pattern-either: + - pattern: var $VAR template.HTML = $EXP + - pattern: var $VAR template.CSS = $EXP + - pattern: var $VAR template.HTMLAttr = $EXP + - pattern: var $VAR template.JS = $EXP + - pattern: var $VAR template.JSStr = $EXP + - pattern: var $VAR template.Srcset = $EXP + message: >- + usage of insecure template types. They are documented as a security risk. See https://golang.org/pkg/html/template/#HTML. + severity: WARNING + metadata: + cwe: + - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')" + references: + - https://golang.org/pkg/html/template/#HTML + - https://twitter.com/empijei/status/1275177219011350528 + category: security + technology: + - template + confidence: LOW + owasp: + - A07:2017 - Cross-Site Scripting (XSS) + - A03:2021 - Injection + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - audit + likelihood: LOW + impact: MEDIUM + languages: + - go diff --git a/go/template/security/ssti.go b/go/template/security/ssti.go new file mode 100644 index 00000000..99269bab --- /dev/null +++ b/go/template/security/ssti.go @@ -0,0 +1,86 @@ +package main + +import ( + "fmt" + "html/template" + "net/http" +) + +type User struct { + ID int + Email string + Password string +} + +func match1(w http.ResponseWriter, req *http.Request) { + + var user1 = &User{1, "user@gmail.com", "Sup3rSecr3t123!"} + query := req.URL.Query().Get("query") + // ruleid:go-ssti + var text = fmt.Sprintf(` + + + SSTI + + +

Hello {{ .Email }}

+

Search result for %s

+ + `, query) + tmpl := template.New("hello") + tmpl, err := tmpl.Parse(text) + if err != nil { + fmt.Println(err) + } + tmpl.Execute(w, user1) +} + +func match2(w http.ResponseWriter, req *http.Request) { + + var user1 = &User{1, "user@gmail.com", "Sup3rSecr3t123!"} + if err := req.ParseForm(); err != nil { + fmt.Fprintf(w, "ParseForm() err: %v", err) + return + } + query := req.Form.Get("query") + // ruleid:go-ssti + var text = fmt.Sprintf(` + + + SSTI + + +

Hello {{ .Email }}

+

Search result for %s

+ + `, query) + tmpl := template.New("hello") + tmpl, err := tmpl.Parse(text) + if err != nil { + fmt.Println(err) + } + tmpl.Execute(w, user1) +} + +func no_match(w http.ResponseWriter, req *http.Request) { + + var user1 = &User{1, "user@gmail.com", "Sup3rSecr3t123!"} + query := "constant string" + // ok:go-ssti + var text = fmt.Sprintf(` + + + SSTI + + +

Hello {{ .Email }}

+

Search result for %s

+ + `, query) + tmpl := template.New("hello") + tmpl, err := tmpl.Parse(text) + if err != nil { + fmt.Println(err) + } + tmpl.Execute(w, user1) +} diff --git a/go/template/security/ssti.yaml b/go/template/security/ssti.yaml new file mode 100644 index 00000000..dcac32e3 --- /dev/null +++ b/go/template/security/ssti.yaml @@ -0,0 +1,56 @@ +rules: +- id: go-ssti + patterns: + - pattern-inside: | + import ("html/template") + ... + - pattern: $TEMPLATE = fmt.Sprintf("...", $ARG, ...) + - patterns: + - pattern-either: + - pattern-inside: | + func $FN(..., $REQ *http.Request, ...){ + ... + } + - pattern-inside: | + func $FN(..., $REQ http.Request, ...){ + ... + } + - pattern-inside: | + func(..., $REQ *http.Request, ...){ + ... + } + - patterns: + - pattern-either: + - pattern-inside: | + $ARG := $REQ.URL.Query().Get(...) + ... + $T, $ERR := $TMPL.Parse($TEMPLATE) + - pattern-inside: | + $ARG := $REQ.Form.Get(...) + ... + $T, $ERR := $TMPL.Parse($TEMPLATE) + - pattern-inside: | + $ARG := $REQ.PostForm.Get(...) + ... + $T, $ERR := $TMPL.Parse($TEMPLATE) + message: >- + A server-side template injection occurs when an attacker is able to use + native template syntax to inject a malicious payload into a template, which is then executed server-side. + When using "html/template" always check that user inputs are validated and sanitized before included + within the template. + languages: [go] + severity: ERROR + metadata: + category: security + cwe: + - 'CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine' + references: + - https://www.onsecurity.io/blog/go-ssti-method-research/ + - http://blog.takemyhand.xyz/2020/05/ssti-breaking-gos-template-engine-to.html + technology: + - go + confidence: MEDIUM + subcategory: + - vuln + likelihood: LOW + impact: HIGH \ No newline at end of file diff --git a/tmp/rules/owasp/injection-raw-html-format.yaml b/tmp/rules/owasp/injection-raw-html-format.yaml new file mode 100644 index 00000000..7eb3c145 --- /dev/null +++ b/tmp/rules/owasp/injection-raw-html-format.yaml @@ -0,0 +1,448 @@ +rules: + - id: raw-html-format + languages: + - go + severity: WARNING + message: Detected user input flowing into a manually constructed HTML string. + You may be accidentally bypassing secure methods of rendering HTML by + manually constructing HTML and this could create a cross-site scripting + vulnerability, which could let attackers steal sensitive user data. Use + the `html/template` package which will safely render HTML instead, or + inspect that the HTML is rendered safely. + metadata: + cwe: + - "CWE-79: Improper Neutralization of Input During Web Page Generation + ('Cross-site Scripting')" + owasp: + - A07:2017 - Cross-Site Scripting (XSS) + - A03:2021 - Injection + category: security + technology: + - go + references: + - https://blogtitle.github.io/robn-go-security-pearls-cross-site-scripting-xss/ + confidence: MEDIUM + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - vuln + likelihood: HIGH + impact: MEDIUM + license: Semgrep Rules License v1.0. For more details, visit + semgrep.dev/legal/rules-license + vulnerability_class: + - Cross-Site-Scripting (XSS) + mode: taint + pattern-sources: + - patterns: + - pattern-either: + - pattern: | + ($REQUEST : *http.Request).$ANYTHING + - pattern: | + ($REQUEST : http.Request).$ANYTHING + - metavariable-regex: + metavariable: $ANYTHING + regex: ^(BasicAuth|Body|Cookie|Cookies|Form|FormValue|GetBody|Host|MultipartReader|ParseForm|ParseMultipartForm|PostForm|PostFormValue|Referer|RequestURI|Trailer|TransferEncoding|UserAgent|URL)$ + pattern-sanitizers: + - pattern: html.EscapeString(...) + pattern-sinks: + - patterns: + - pattern-either: + - pattern: fmt.Printf("$HTMLSTR", ...) + - pattern: fmt.Sprintf("$HTMLSTR", ...) + - pattern: fmt.Fprintf($W, "$HTMLSTR", ...) + - pattern: '"$HTMLSTR" + ...' + - metavariable-pattern: + metavariable: $HTMLSTR + language: generic + pattern: <$TAG ... + - id: session-cookie-missing-secure + patterns: + - pattern-not-inside: | + &sessions.Options{ + ..., + Secure: true, + ..., + } + - pattern: | + &sessions.Options{ + ..., + } + message: A session cookie was detected without setting the 'Secure' flag. The + 'secure' flag for cookies prevents the client from transmitting the cookie + over insecure channels such as HTTP. Set the 'Secure' flag by setting + 'Secure' to 'true' in the Options struct. + metadata: + cwe: + - "CWE-614: Sensitive Cookie in HTTPS Session Without 'Secure' Attribute" + owasp: + - A05:2021 - Security Misconfiguration + references: + - https://github.com/0c34/govwa/blob/139693e56406b5684d2a6ae22c0af90717e149b8/user/session/session.go#L69 + category: security + technology: + - gorilla + confidence: MEDIUM + subcategory: + - audit + likelihood: LOW + impact: LOW + license: Semgrep Rules License v1.0. For more details, visit + semgrep.dev/legal/rules-license + vulnerability_class: + - Cookie Security + fix-regex: + regex: (Secure\s*:\s+)false + replacement: \1true + severity: WARNING + languages: + - go + + - id: session-cookie-samesitenone + patterns: + - pattern-inside: | + &sessions.Options{ + ..., + SameSite: http.SameSiteNoneMode, + ..., + } + - pattern: | + &sessions.Options{ + ..., + } + message: Found SameSiteNoneMode setting in Gorilla session options. Consider + setting SameSite to Lax, Strict or Default for enhanced security. + metadata: + cwe: + - "CWE-1275: Sensitive Cookie with Improper SameSite Attribute" + owasp: + - A05:2021 - Security Misconfiguration + references: + - https://pkg.go.dev/github.com/gorilla/sessions#Options + category: security + technology: + - gorilla + confidence: MEDIUM + subcategory: + - audit + likelihood: LOW + impact: LOW + license: Semgrep Rules License v1.0. For more details, visit + semgrep.dev/legal/rules-license + vulnerability_class: + - Cookie Security + fix-regex: + regex: (SameSite\s*:\s+)http.SameSiteNoneMode + replacement: \1http.SameSiteDefaultMode + severity: WARNING + languages: + - go + + - id: session-cookie-missing-secure + patterns: + - pattern-not-inside: | + &sessions.Options{ + ..., + Secure: true, + ..., + } + - pattern: | + &sessions.Options{ + ..., + } + message: A session cookie was detected without setting the 'Secure' flag. The + 'secure' flag for cookies prevents the client from transmitting the cookie + over insecure channels such as HTTP. Set the 'Secure' flag by setting + 'Secure' to 'true' in the Options struct. + metadata: + cwe: + - "CWE-614: Sensitive Cookie in HTTPS Session Without 'Secure' Attribute" + owasp: + - A05:2021 - Security Misconfiguration + references: + - https://github.com/0c34/govwa/blob/139693e56406b5684d2a6ae22c0af90717e149b8/user/session/session.go#L69 + category: security + technology: + - gorilla + confidence: MEDIUM + subcategory: + - audit + likelihood: LOW + impact: LOW + license: Semgrep Rules License v1.0. For more details, visit + semgrep.dev/legal/rules-license + vulnerability_class: + - Cookie Security + fix-regex: + regex: (Secure\s*:\s+)false + replacement: \1true + severity: WARNING + languages: + - go + + - id: session-cookie-missing-httponly + patterns: + - pattern-not-inside: | + &sessions.Options{ + ..., + HttpOnly: true, + ..., + } + - pattern: | + &sessions.Options{ + ..., + } + message: A session cookie was detected without setting the 'HttpOnly' flag. The + 'HttpOnly' flag for cookies instructs the browser to forbid client-side + scripts from reading the cookie which mitigates XSS attacks. Set the + 'HttpOnly' flag by setting 'HttpOnly' to 'true' in the Options struct. + metadata: + cwe: + - "CWE-1004: Sensitive Cookie Without 'HttpOnly' Flag" + owasp: + - A05:2021 - Security Misconfiguration + references: + - https://github.com/0c34/govwa/blob/139693e56406b5684d2a6ae22c0af90717e149b8/user/session/session.go#L69 + category: security + technology: + - gorilla + confidence: MEDIUM + subcategory: + - audit + likelihood: LOW + impact: LOW + license: Semgrep Rules License v1.0. For more details, visit + semgrep.dev/legal/rules-license + vulnerability_class: + - Cookie Security + fix-regex: + regex: (HttpOnly\s*:\s+)false + replacement: \1true + severity: WARNING + languages: + - go + + - id: cookie-missing-secure + patterns: + - pattern-not-inside: | + http.Cookie{ + ..., + Secure: true, + ..., + } + - pattern: | + http.Cookie{ + ..., + } + message: A session cookie was detected without setting the 'Secure' flag. The + 'secure' flag for cookies prevents the client from transmitting the cookie + over insecure channels such as HTTP. Set the 'Secure' flag by setting + 'Secure' to 'true' in the Options struct. + metadata: + cwe: + - "CWE-614: Sensitive Cookie in HTTPS Session Without 'Secure' Attribute" + owasp: + - A05:2021 - Security Misconfiguration + references: + - https://github.com/0c34/govwa/blob/139693e56406b5684d2a6ae22c0af90717e149b8/util/cookie.go + - https://golang.org/src/net/http/cookie.go + category: security + technology: + - go + confidence: MEDIUM + subcategory: + - vuln + likelihood: LOW + impact: LOW + license: Semgrep Rules License v1.0. For more details, visit + semgrep.dev/legal/rules-license + vulnerability_class: + - Cookie Security + fix-regex: + regex: (Secure\s*:\s+)false + replacement: \1true + severity: WARNING + languages: + - go + + - id: go.lang.security.audit.net.cookie-missing-httponly.cookie-missing-httponly + patterns: + - pattern-not-inside: | + http.Cookie{ + ..., + HttpOnly: true, + ..., + } + - pattern: | + http.Cookie{ + ..., + } + message: A session cookie was detected without setting the 'HttpOnly' flag. The + 'HttpOnly' flag for cookies instructs the browser to forbid client-side + scripts from reading the cookie which mitigates XSS attacks. Set the + 'HttpOnly' flag by setting 'HttpOnly' to 'true' in the Cookie. + metadata: + cwe: + - "CWE-1004: Sensitive Cookie Without 'HttpOnly' Flag" + owasp: + - A05:2021 - Security Misconfiguration + references: + - https://github.com/0c34/govwa/blob/139693e56406b5684d2a6ae22c0af90717e149b8/util/cookie.go + - https://golang.org/src/net/http/cookie.go + category: security + technology: + - go + confidence: MEDIUM + subcategory: + - vuln + likelihood: LOW + impact: LOW + license: Semgrep Rules License v1.0. For more details, visit + semgrep.dev/legal/rules-license + vulnerability_class: + - Cookie Security + fix-regex: + regex: (HttpOnly\s*:\s+)false + replacement: \1true + severity: WARNING + languages: + - go + + - id: session-cookie-samesitenone + patterns: + - pattern-inside: | + &sessions.Options{ + ..., + SameSite: http.SameSiteNoneMode, + ..., + } + - pattern: | + &sessions.Options{ + ..., + } + message: Found SameSiteNoneMode setting in Gorilla session options. Consider + setting SameSite to Lax, Strict or Default for enhanced security. + metadata: + cwe: + - "CWE-1275: Sensitive Cookie with Improper SameSite Attribute" + owasp: + - A05:2021 - Security Misconfiguration + references: + - https://pkg.go.dev/github.com/gorilla/sessions#Options + category: security + technology: + - gorilla + confidence: MEDIUM + subcategory: + - audit + likelihood: LOW + impact: LOW + license: Semgrep Rules License v1.0. For more details, visit + semgrep.dev/legal/rules-license + vulnerability_class: + - Cookie Security + fix-regex: + regex: (SameSite\s*:\s+)http.SameSiteNoneMode + replacement: \1http.SameSiteDefaultMode + severity: WARNING + languages: + - go + + - id: use-of-md5 + message: Detected MD5 hash algorithm which is considered insecure. MD5 is not + collision resistant and is therefore not suitable as a cryptographic + signature. Use SHA256 or SHA3 instead. + languages: + - go + severity: WARNING + metadata: + owasp: + - A03:2017 - Sensitive Data Exposure + - A02:2021 - Cryptographic Failures + cwe: + - "CWE-328: Use of Weak Hash" + source-rule-url: https://github.com/securego/gosec#available-rules + category: security + technology: + - go + confidence: MEDIUM + references: + - https://owasp.org/Top10/A02_2021-Cryptographic_Failures + subcategory: + - vuln + likelihood: MEDIUM + impact: MEDIUM + license: Semgrep Rules License v1.0. For more details, visit + semgrep.dev/legal/rules-license + vulnerability_class: + - Insecure Hashing Algorithm + patterns: + - pattern-inside: | + import "crypto/md5" + ... + - pattern-either: + - pattern: | + md5.New() + - pattern: | + md5.Sum(...) + + - id: formatted-template-string + message: Found a formatted template string passed to 'template.HTML()'. + 'template.HTML()' does not escape contents. Be absolutely sure there is no + user-controlled data in this template. If user data can reach this + template, you may have a XSS vulnerability. + metadata: + cwe: + - "CWE-79: Improper Neutralization of Input During Web Page Generation + ('Cross-site Scripting')" + owasp: + - A07:2017 - Cross-Site Scripting (XSS) + - A03:2021 - Injection + references: + - https://golang.org/pkg/html/template/#HTML + category: security + technology: + - go + confidence: MEDIUM + cwe2022-top25: true + cwe2021-top25: true + subcategory: + - audit + likelihood: LOW + impact: MEDIUM + license: Semgrep Rules License v1.0. For more details, visit + semgrep.dev/legal/rules-license + vulnerability_class: + - Cross-Site-Scripting (XSS) + languages: + - go + severity: WARNING + patterns: + - pattern-not: template.HTML("..." + "...") + - pattern-either: + - pattern: template.HTML($T + $X, ...) + - pattern: template.HTML(fmt.$P("...", ...), ...) + - pattern: | + $T = "..." + ... + $T = $FXN(..., $T, ...) + ... + template.HTML($T, ...) + - pattern: | + $T = fmt.$P("...", ...) + ... + template.HTML($T, ...) + - pattern: | + $T, $ERR = fmt.$P("...", ...) + ... + template.HTML($T, ...) + - pattern: | + $T = $X + $Y + ... + template.HTML($T, ...) + - pattern: |- + $T = "..." + ... + $OTHER, $ERR = fmt.$P(..., $T, ...) + ... + template.HTML($OTHER, ...) diff --git a/tmp/rules/owasp/plaintext-http-link.yaml b/tmp/rules/owasp/plaintext-http-link.yaml new file mode 100644 index 00000000..48e50365 --- /dev/null +++ b/tmp/rules/owasp/plaintext-http-link.yaml @@ -0,0 +1,33 @@ +rules: + - id: plaintext-http-link + metadata: + category: security + technology: + - html + cwe: + - "CWE-319: Cleartext Transmission of Sensitive Information" + owasp: + - A03:2017 - Sensitive Data Exposure + - A02:2021 - Cryptographic Failures + confidence: HIGH + subcategory: + - vuln + references: + - https://cwe.mitre.org/data/definitions/319.html + likelihood: LOW + impact: LOW + license: Semgrep Rules License v1.0. For more details, visit + semgrep.dev/legal/rules-license + vulnerability_class: + - Mishandled Sensitive Information + patterns: + - pattern: ... + - metavariable-regex: + metavariable: $URL + regex: ^(?i)http:// + message: This link points to a plaintext HTTP URL. Prefer an encrypted HTTPS URL + if possible. + severity: WARNING + languages: + - html +