OWASP Top 10 & Go

OWASP Top 10

The Open Web Application Security Project (OWASP) publishes a list of the top ten most critical web application security risks.

The OWASP Top 10 is in the process of being revamped since the last release in 2013. A release candidate was published on April 10th, 2017 and is planned to be released in July or August of 2017 after taking comments from the public which ends on June 30th.

The changes made by OWASP from 2013 to 2017 include:

  • Merging A4: Insecure Direct Object References and A7: Missing Function Level Access Control into A4: Broken Access Control
  • Added A7: Insufficient Attack Protection
  • Dropped A10: Unvalidated Redirects and Forwards
  • Added A10: Under-protected APIs

For more information regarding the changes of the Top 10 from 2013 to 2017, you can view the release candidate at owasp.org, the changes are listed under the release notes section.

Go

Go is a free and open source programming language created by Google. It first appeared on November 10th, 2009. It is a compiled, statically typed, garbage-collected programming language. It has become a popular option for developers wishing to make web applications due to its native HTTP protocol support, allowing for quick development.

Go has built-in tooling for package management. Fetching a package is as simple as typing

go get http://example.com

godoc.org hosts all documentation for packages that are hosted on Bitbucket, Github, Google Project Housing, and Launchpad. It will quickly become your friend while writing Go code.

Let’s jump into the OWASP Top 10.

A1 — SQL Injection

SQL injection can result in loss of data, the release of confidential information, and loss of server access among other issues. With Go, you can use prepared statements to avoid SQL injection. This prevents user input from escaping from the statement and executing another.

It is still possible to have SQL injection occur with Go, if you are manually creating the statements for the database by concatenating a string with unsanitary user input. This is heavily discouraged and you should not be doing this. Prepared statements will deal with SQL injection, and you should use them whenever you can.


import (
	"database/sql"
	_ "github.com/go-sql-driver/mysql" // database driver
)

var name string
var avatar string
var userInput string // Input from client

// UNSAFE, concatenating strings without sanitizing input
err := db.QueryRow("SELECT name, avatar FROM users WHERE id=" + userInput).Scan(&name, &avatar)
if err != nil {
	panic(err)
}
// Prepared statements, SAFE from injection
// Using placeholder parameters means Go will use prepared statements under the hood
err := db.QueryRow("SELECT name, avatar FROM users WHERE id=?", userInput).Scan(&name, &avatar)
if err != nil {
	panic(err)
}

// The ? placeholder in the above statement is dependent on the database driver you use
// For go-sql-driver/mysql question marks are used

A2 — Broken Authentication and Session Management

It is very important to properly manage sessions and the authentication of users. In the example below I will use randomly generated tokens, which are given to users that login. They will then use this token for every request thereafter.

Let’s start off with the user login, it receives a JSON payload which contains the username and password of the user attempting to log in.

func loginHandler(w http.ResponseWriter, r *http.Request) {
	type jsonLoginInfo struct {
		Username string `json:"username"`
		Password string `json:"password"`
	}
	var jl jsonLoginInfo
	decoder := json.NewDecoder(r.Body)
	if err := decoder.Decode(&jl); err != nil {
		panic(err)
	}
	// json decoder has populated our struct with the payload information.
	

	// Fetch user from database.
	var dbPassword []byte
	var dbUserID int
	err := db.QueryRow("SELECT id, password FROM users WHERE username=?", jl.Username).Scan(&dbUserID, &dbPassword)
	switch err {
	case err == sql.ErrNoRows:
		// Unknown user
		http.Error(w, "login-failed", http.StatusForbidden)
		// Go does not need breaks in switches, you can use the keyword fallthrough to make a
		// switch case continue through to the next case.
		return
	case err != nil:
		// DB error.
		http.Error(w, "login-failed", http.StatusInternalServerError)
		return
	}

	// Authenticate user, this is simplified for the example!
	// We will look at securing data in A6.
	if jl.Password != dbPassword {
		http.Error(w, "login-failed", http.StatusForbidden)
		return
	}

	// Lets give the user a token now.
	// For this example, I will have a table in the SQL database that will contain user's authentication
	// tokens, which holds their user id, the token itself, and a timestamp of when
	// it was created.

	var arr [32]byte
	_, err = rand.Read(arr[:]) // Generate 32 random bytes
	if err != nil {
		http.Error(w, "login-failed", http.StatusInternalServerError)
		return
	}
    
	// Here we convert our random bytes into a base64 encoded string, this is our token.
	token := base64.URLEncoding.EncodeToString(arr[:])

	_, err = db.Exec("INSERT INTO access_tokens(access_token, user_id, created_at) VALUES(?, ?, UTC_TIMESTAMP)", token, dbUserID)
	if err != nil {
		// Inserting into database failed.
		http.Error(w, "login-failed", http.StatusInternalServerError)
		return
	}

	jsonAuthToken := struct {
		Token string `json:"token"`
	}{
		Token: token,
	}

	// Send token to client as JSON with the encoder.
	if err = json.NewEncoder(w).Encode(&jsonAuthToken); err != nil {
		http.Error(w, "login-failed", http.StatusInternalServerError)
		return
	}
	// Sent user their token, which will be used for every request and authenticated by the server.
}

In the above code, you can see we authenticate the user and then give them a token they can use to identify themselves when they make further requests. The client should store the token for later use.

Now let’s look at a function to verify the token.

func checkToken(token string) (*userInfo, error) {
	ret := &userInfo{}
	var created time.Time

	err := db.QueryRow("SELECT user_id, created_at FROM access_tokens WHERE access_token=?", token).Scan(&ret.userID, &created)

	switch {
	case err == sql.ErrNoRows: // Invalid token
		return nil, nil
	case err != nil: // DB error
		return nil, err
	}
	
	// Good idea to check token created time vs EOL.
	if time.Since(created).Hours() > 4 { // expire after 4 hours.
		if _, err := db.Exec("DELETE FROM access_tokens WHERE access_token=?", token); err != nil {
			return nil, err
		}
		return nil, nil // Expired token
	}

	// Token is good, we can get user information (username) from database.
	err = db.QueryRow("SELECT username FROM users WHERE id=?", ret.userID).Scan(&ret.username)
	if err != nil { // DB error
		return nil, err
	}

	return ret, nil // All okay, return user information.
}

We query the token from the access_tokens table and check who owns it, and then query other information from the user table since we know who they are now. (In this case we’re just getting their username)

Go functions allow multiple return values, if everything is okay we return our structure with user data, and nil for the error. Otherwise we return nil for the user structure and our error. The userInfo structure just holds a username and ID for our example.

In the case there is no error, but the token is either invalid or expired, we return nil for both error and the userInfo structure.

type userInfo struct {
	username string
	userID int
	// Any other user related fields the application may need.
}

Now, whenever the user wants to perform an authenticated action, they supply their token and we can call checkToken before allowing them to do so.

func actionHandler(w http.ResponseWriter, r *http.Request) {
	type jsonAction struct {
		Token string `json:"token"`
		// Other information required for action
	}
	var ja jsonAction

	decoder := json.NewDecoder(r.Body)
	if err := decoder.Decode(&ja); err != nil {
		panic(err) // Decoder fail
	}

	uInfo, err := checkToken(ja.Token)
	if err != nil || uInfo == nil {
		// Invalid token, expired or non-existent.
		// Possible to check error to give a more specific message to user.
		http.Error(w, "bad-auth", http.StatusForbidden)
		return
	}
	fmt.Print("User " + uInfo.username + " wants to perform action.")
	// Perform authenticated action . . .
}

The user attempts to do an authenticated action, we check their token and if everything is okay, we can carry on.

This is just an simple example of a authentication and session handling.

A3 — Cross-Site Scripting (XSS)

Malicious input can cause a lot of issues if it not properly escaped and handled. Malicious code for instance could expose our authorization token and lead to forged requests.

Always escape and be aware of how you are handling users data. Below is an example of XSS vulnerable code.

func indexHandler(w http.ResponseWriter, r *http.Request) {
	// This is unsafe, if a script is provided it will be executed
	answer := "<h1>Hello, " + r.URL.Query().Get("name") + "</h1>"
	io.WriteString(w, answer)
}

func main() {
	http.HandleFunc("/", indexHandler)
	http.ListenAndServe(":8080", nil)
}

If I visited this site, with <script>alert(1)</script> as the name parameter, it would execute. Now imagine if some malicious code instead printed privileged information from local storage.

In Go, the ‘html/template’ package offers methods to escape user input, as well as html templating that will automatically escape user input for you.

import "html/template"

func indexHandler(w http.ResponseWriter, r *http.Request) {
	answer = "<h1>Hello, " + html.EscapeString(r.URL.Query().Get("name")) + "</h1>"
	// This will escape the script and print it as text.
	io.WriteString(w, answer)
}

Another option is templating, you can learn more about it at https://golang.org/pkg/html/template/

A4 — Broken Access Control

If we have do not have proper access control, users with less privileges may be able to perform the actions of other users with more privileges, such as administrators.

We will expand on our example earlier, and add a field to our user info structure to indicate whether or not the user is an administrator.

type userInfo struct {
        . . .
        admin bool // Populate this field when we verify the user's token.
}

Now if we want to perform an elevated action, we should check against this first.

func elevatedAction(w http.ResponseWriter, r *http.Request) {
	. . .
	if !uInfo.admin {
		http.Error(w, "bad-auth", http.StatusForbidden)
		return
	}
}
// Perform administrative action . . .

A5 — Security Misconfiguration

Security misconfiguration mostly relates to improperly setting up of our web application or database, anything that is being used by our application. For example, leaving the default username and password for our database, or having our firewall improperly configured. Make sure you properly set up your server before opening it to the public.

A6 — Sensitive Data Exposure

Sensitive data should always be securely stored and transferred. There are many native Go packages that implement different encryption algorithms. For example I will use the bcrypt package, which implements Provos and Mazières’s bcrypt adaptive hashing algorithm.

It is also important to use TLS to encrypt data between server and client, you can do this with Go if you are not using a reverse proxy such as Nginx to handle it for you by doing

http.ListenAndServeTLS(":8080", /path/to/certfile.pem, /path/to/privatekey.pem, nil)

You can check out the http documentation for more information regarding TLS.

Say for example we wanted to encrypt a user’s password before it was entered into a database.

import "golang.org/x/crypto/bcrypt"

var password string // password select by user
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
	panic(err)
}

This will give us our hashed password we can insert into the database, and later when we want to authenticate the user when they attempt to login we can do as follows.

var dbPassword string // Password from database
var userPassword string // user input
err := bcrypt.CompareHashAndPassword(dbPassword, []byte(userPassword))
if err != nil { // Incorrect password
	http.Error(w, "bad-auth", http.StatusForbidden)
	return
}

This way, in the unfourtunate event that your database is compromised, passwords can not be read as plain text.

This method can also be used in our first example regarding SQL, where we just checked if the passwords were identical.

A7 — Insufficient Attack Protection

There are many types of attacks a web application may come across, and doing what you can to defend against them helps. For example, abnormal requests from the client may be a attacker crafting requests to try and break your API. Identifying these requests and blocking them can help keep your application secure.

For example, if one of our endpoints had an expected range or value, we could ignore them if they did not meet our expected values.

A8 — Cross-Site Request Forgery (CSRF)

Malicious sites may try to forge requests to our application, performing actions against the user’s will. A common example is a vulnerable banking application. Imagine a banking web application at www.ourbankapp.com, and the user is currently logged in.

Imagine if the malicious website redirected our user to www.ourbankapp.com/transfer?ACCT=1923912&AMOUNT=500

This would cause our application to send $500 from the user’s account to an attackers. This forged request from the malicious website transferred money into their account against the user’s will.

Our example above incorporates a token into every request, as long as this is properly handled on the client side we are safe from CSRF. For instance, the token could be stored in local storage, and when we the user is redirected to our site maliciously, the token will not be available in the request and the money will not be sent.

This ties into A3 — Cross-Site Scripting, if a malicious user is able to execute code on our website, they could access our token. It is very important to prevent all attack vectors into your web application, a small vulnerability may eventually lead to a much bigger problem.

A9 — Using Components with Known Vulnerabilities

Depending on the vulnerabilities, this can cause some serious problems for your application. You should always keep your components up to date. If there are known vulnerabilities in components you are using, you should prevent them for either being encountered, or find a different solution.

Subscribing to any third-party packages you use and updating them when new releases are available will help keep your web application safe.

A Go package can be updated by executing

go get -u http://example.com

A10 — Under-protected APIs

A malicious user may send fraudulent requests to your API. It is your job to make sure that every request is authenticated and secure. You must ensure every request is legitimate, for example by using a token like we did earlier. You must also ensure the token is secure by preventing attack vectors such as XSS and CSRF. It is important to always monitor your application and to fix any vulnerabilities before they are taken advantage of.


Protecting against the OWASP Top 10 is a step in the right direction towards secure web applications. OWASP contains a great number of resources that deal with web application security, and I’d recommend visiting the site to further improve your knowledge and the security of your web application.

Share Comments
comments powered by Disqus