Hardening guides
Preamble & credits:
Author: Emmanuel Odeke, Orijtech Inc.
Contributors & Editors: Nathan Dias, Jean-Philippe Aumasson, Chainguard Inc by virtue of their Q2 2022 Supply chain assessment for Cosmos
Target audience: Cosmos Ecosystem https://cosmos.network/ and Go developers
Versioning
Version | Start date | End date | Comments | Authors | Editors |
v1.0.0 | Saturday-12th-August-2022 | Tuesday-23rd-August-2022 | Emmanuel Odeke | Nathan Dias Jean-Philippe Aumasson |
Testing:
- Run tests on your code and check for code coverage: aim for 100% coverage with diverse inputs. Go supports coverage checking with https://go.dev/blog/cover
$ # Firstly run the tests and collect coverage into out.cover $ go test -coverprofile=out.cover ./... $ # Next render the coverage for visual inspection in your browser. $ go tool cover -html=out.cover
Fuzzing:
What is fuzzing?
Fuzzing is the art of employing randomized state to generate more test cases than can be cognitively crafted by a human. Most modern fuzzing is coverage based in that it is applied code paths and tries to mutate known inputs while being guided with whatâs similar to good inputs that cover more new paths. Go 1.18 (introduced in March 2022) and above has native support for fuzzing per https://go.dev/blog/go1.18 but these are prior guides for fuzzing in Go https://go.dev/doc/fuzz/ We use oss-fuzz for continously fuzzing cosmos/cosmos-sdk and tendermint/tendermint and when get informed of continuously presented bugs.
Fuzz trophies:
The impetus to fuzz is the wide spectrum of bugs that can be uncovered just by guiding the fuzzerâs genetic algorithm with inputs. Here is a glimpse into some of the long lists of critical bugs that go-fuzz by the excellent Dmitry Vyukov has uncovered https://github.com/dvyukov/go-fuzz#trophies in a variety of critical libraries.
Goroutine leaks:
- Ensure that all the goroutines terminate deterministically when your test exits
- Pass in context.Context into functions and goroutines. With context.Context passed in, then multiplex on âcontext.Done() to detect whether a context was canceled or deadline exceeded so that you can cancel work. Please read this blogpost for more information https://go.dev/blog/context
- Multiplex as much on receiving channels using the second value to check if the channel is still open so
v, stillOpen := <-rCh
instead ofv := <-rCh
func doWorkGood(ctx context.Context, jobsCh <-chan *W, do func(ctx context.Context, *W)) {
for {
// Otherwise carry on doing the work
select {
case w, stillOpen := <-jobsCh:
if !stillOpen {
return
}
do(ctx, w)
case <-ctx.Done():
return
}
}
}
func doWorkBad(jobsCh <-chan *W, do func(*W)) {
for w := <-jobsCh {
do(w)
}
}
File permissions:
UNIX file permissions are expressed in Octal (base 8) as most file permissions cover 3 classifications of users:
- The user that owns the file (âuserâ)
- Users who are part of the group that owns the file (âgroupâ)
- Users who are not the owner nor a member of the âgroupâ (âotherâ)
Please see https://en.wikipedia.org/wiki/File-system_permissions#Traditional_Unix_permissions
Example permissions can be 0
o
755
or just 0755
which can be interpreted as:
- 7 which when splayed as binary digits: 111 â RWX (Read-Write-Execute)
- 5 which when splayed as binary digits: 101 â R-X (Read- -Execute)
and collectively 0755 and 0600 mean
Splayed | Current user/owner | Group | Others | |
0755 | 111-101-101 | Read-Write-Execute | Read, Execute | Read, Execute |
0600 | 110-000-000 | Read-Write | No access | No access |
Below is an illustration of what they are like and how to interpret octal UNIX permissions
- Restrict permissions to reduce unnecessary permissions so for example prefer
0600
instead of0755
when creating files that arenât executable binaries and if users in your group complain, sure give them read access in their group mask to say0640
and if it is an executable, sure then go for say0700
until folks complain then surely given them say0740
Signal handling:
- To properly handle signals, the channel passed into signal.Notify MUST be buffered aka
make(chan os.Signal, 1)
and NOTmake(chan os.Signal)
runninggo vet
should flag this as we open source the static analyzersigchanyzer
to the Go tool chain per https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/sigchanyzer and it is available when you rungo test
Data races & synchronization:
- Please ensure that you run your tests using the Go race detector i.e.
go test -race ./...
- Please avoid using
sync.RWMutex
as it is commonly misused in scenarios that one canât tell whether a read is occurring or not; it is hard to use and to predict the scenarios in which itâll be sufficient, one needs to carefully analyze their code. In majority of the cases, just please usesync.Mutex
and here is some justification https://gist.github.com/odeke-em/234bc88fbaf9e439b70a618ce6674659
Building binaries:
- Prefer to use the container image builder
ko
which is available at https://github.com/google/ko instead of building binaries locally
Container images:
- Prefer to use smaller/skimmed images with smaller attack surface areas, with vulnerability scanners such as
distroless.dev/static
and that are well maintained. Please see https://github.com/cosmos/gaia/issues/1621
- Please avoid running Docker containers as root whenever possible, please use
distroless.dev/static
which easily mitigates the need to run as root
Integer overflows and underflows:
Integer overflows occur when a numeric value is shoved into the limited register width precision/range of an integer type and thereby changes sign or interpreted value due to warping. Integers are categorized as signed or unsigned. Signed means that it can be negative or positive; while unsigned means that it can at minimum be equal to +0 and thus only positive within its maximum range. The register width determines how data is interpreted for various primitive times. For signed values, the register has to maintain a carry bit, while for unsigned values, there is no need to maintain the carry bit, which is why
Type | Minimum value | Maximum value |
Signed int 32 bits | -2147483648 | 2147483647 |
Unsigned int 32 bits | 0 | 4294967295 |
Signed int 64 bits | -9223372036854775808 | 9223372036854775807 |
Unsigned int 64 bits | 0 | 18446744073709551615 |
If we think of a register width as a clock, warping happens by continuing clockwise
The minimum value of a 32-bit signed integer is: -(1<<31) -2147483648
and the maximum value is: ((1<<31) -1) 2147483647
thus if an untyped constant value greater than ((1<<31)-1) is shoved into an integer value for storage, its value warps around to the alternating signs while being deducted from maxInt until the deficit fits within that range.
Integer overflows can occur when uint* values are cast to int* values and likewise when int* value are cast to uint* This usually happens when values that are out of the respective register width.
Original type & value | Cast type and resulting value | Overflow | Reason |
int64(-1) | uint64(x) = 18446744073709551615 | Yes | The bit pattern of -1 is 0b1111111111111111111111111111111111111111111111111111111111111111 which when interpreted for unsigned values is the maximum unsigned value of 0xffffffffffffffff; due to the warping around |
uint64(1<<63) | int64(x) = -9223372036854775808 | Yes | The value x overflows the max range of int64 when warped passed ((1<<63)-1) falls on the minimum value of int64 due to the extra +1 |
int32(-1) | uint32(x) = 4294967295 | Yes | The value x is negative and when its bit pattern is interpreted and stored in the register for a uint32 |
int64(2000) | uint64(x) = 2000 | No | The value x is in the range of uint64 |
Fix
To protect yourself against overflows, avoid blindly casting values between unsigned types. Ensure you set guards against the minimum and maximum values and reject values out of the correct ranges. We have found bugs that resulted from callously casting from int* to uint* values from unchecked bounds code
Periodic supply chain analysis for dependencies:
Our supply chain security partner and vendor Chainguard Inc periodically conducts supply chain security audits of the Cosmos ecosystem and many related supply chains. For July 2022 aka Q2 2022, they produced a comprehensive report per https://drive.google.com/file/d/1BCDUSZ3cSdO8FTD9A-nA21_iViONoFln/view which contains plenty of gems related to not only the Cosmos ecosystem but for general secure software supply chain security. We highly recommend that everyone read the report and internalize the suggestions which include tightening security and checks using much better processes like more security containers, using Chainguard Enforce, OpenSSF score cards to increase the âSupply chain Levels for Software Artificatsâ aka SLSA compliance levels.
Maps:
- Prefer to create maps using
make
instead of by variable declaration without assignment, because
var m map[string]any
// Then later on in code, accidentally set without
// initialization
m["key"] = "value
will panic per https://go.dev/play/p/iAy0L-Iykm4 with
panic: assignment to entry in nil map
goroutine 1 [running]:
main.set(...)
/tmp/sandbox2664608898/prog.go:4
main.main()
/tmp/sandbox2664608898/prog.go:10 +0x49
improperly initialized
package main func set(m map[int]any, key, value int) { m[key] = value } func main() { var m map[int]any for i := 0; i < 10; i++ { set(m, i, i) } }
the proper fix would be https://go.dev/play/p/fr4jp8sQxv0 or inlined below
properly initialized
package main func set(m map[int]any, key, value int) { m[key] = value } func main() { m := make(map[int]any) for i := 0; i < 10; i++ { set(m, i, i) } }
Permissions on already opened file handles might not reflect even after modifying operations like .Chmod, .Close
TL;DR: If I have Read-Write-Execute permissions on a file and I opened a handle with those permissions successfully, until I close, discard that handle then re-acquire it, the initial permissions will be retained and even if you change the permissions to no access, Iâll still be able to read, write and execute that file.
- Be mindful of expectations when you have open file handles, that until you close and freshly open a file handle, it retains its prior permissions. This happens due to the way UNIX file systems are built and it is a deficiency that is basically a surprise. The consequence of that is that if a handle was taken and is open before file permission updates were made, that handle can still write or modify that file without the new permissions restricting them, on most *NIX platforms.
Here is an exhibit in a variety of programming languages https://gist.github.com/odeke-em/4bf530f3dc6bb4993a1f1d12dac72d46 of the problem
Understand the order of package loading in Go:
TL;DR: When auditing supply chains and to figure out attack susceptibility, ensure you understand the order in which packages are loaded per the Go specification
In the Go specification and compliant implementations, packages are loaded in declaration order and further loaded sooner the less variables that they depend on, per https://go.dev/ref/spec#Package_initialization and basically determined by the order in which files are presented to the compiler
Cryptographic randomness:
TL;DR: Always prefer to use crypto/rand.Reader or crypto/rand.* to seed functions that require strong randomness. Avoid math/rand functions as much as you can, because they create predictable sequences with low entropy. For example, use cryptographic randomness for: generating cryptographic keys, secret tokens, temporary passphrases, session IDs.
Randomness backdoor sanity test:
In Go, variables can be swapped out at runtime. The very important and highly venerated crypto.Reader is used by all cryptographic functions in the Go standard library and in many important libraries like hashing algorithms plus core infrastructure. By definition given that crypto/rand.Reader is just any other variable, it is mutable by changing its reference so the following code is valid and will have an actual effect of changing every use of crypto/rand.Reader to have the new behavior. PLEASE be ware of this supply chain attack!
import "crypto/rand"
type panickingReader int
func (pr *panickingReader) Read(b []byte) (int, error) {
panic("all I do is panic!")
}
func init() {
rand.Reader = new(panickingReader)
}
Fix/Suggestion:
To guard against such issues it is highly recommended that before critical functions, if feeling extra paranoid, please make sure to perform a sanity check on crypto/rand.Reader such as in your cmd/*/main.go file which isnât a package and canât be loaded (aka is a terminal point in your init function that you control), then please run for example per https://go.dev/play/p/Zfk0-rXwZcZ
mitigation_exhibit.go
package main import ( "crypto/rand" "fmt" ) type riggedReader int var oldRandReader = rand.Reader func (rr *riggedReader) Read(b []byte) (int, error) { if len(b) != 32 && len(b) != 64 { return oldRandReader.Read(b) } for i := range b { b[i] = 0 } return len(b), nil } func init() { // Prefix the function with this assignment to take advantage of package initialization // so that oldRandReader can load and retain the reference to the true random reader. _ = oldRandReader // Rig rand.Reader rand.Reader = new(riggedReader) } func sanityCheckOnRandReader() { randCounts := make(map[string]int) sizes := []int{2, 10, 29, 32, 64, 66, 127, 128, 256, 512, 1 << 10, 2 << 10} for i := 0; i < 10; i++ { for _, size := range sizes { b := make([]byte, size) n, err := rand.Reader.Read(b) if err != nil { panic(err) } // Anytime that the reader returns the is a clash, we shall encounter > 1 entry key := string(b[:n]) randCounts[key] += 1 if randCounts[key] > 1 { panic(fmt.Sprintf("Encountered clashing data from crypto/rand.Reader with size: %d", size)) } } } } func init() { sanityCheckOnRandReader() } func main() {}
crypto/tls: avoid using tls.Config.InsecureSkipVerify
Many servers out there that use TLS might be tempted to use crypto/tls.Config.InsecureSkipVerify so as to allow for server tests to run locally, but then mistakenly share that code with production services. With that flag set, any certificate can be mis/presented and this allows MITM attacks due to no verifications :-(
Fix
Instead, prefer to use net/http/httptest.NewTLSServer in tests for TLS servers like this
package main import ( "fmt" "io" "net/http" "net/http/httptest" ) func main() { cst := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { fmt.Fprintln(rw, "Hello, coding & security hardening!") })) defer cst.Close() client := cst.Client() res, err := client.Get(cst.URL) if err != nil { panic(err) } defer res.Body.Close() greeting, err := io.ReadAll(res.Body) if err != nil { panic(err) } fmt.Printf("%s\n", greeting) }
Use html/template to generate HTML to avoid security exploits like XSS
It might be tempting and seemingly most convenient to generate HTML just by string concatenation, but it is much safer to use html/template which is built to allow code pipelines and be safe against code injection. This packages allows HTML to be dynamically generated by passing in what could be untrusted data into template.Execute as data inputs. The html/package package has been hardened for security to allow escaping and here is a demo that is runnable locally
Exhibit
pwn_server.go
// cyber.orijtech.com exhibit for why in Go, you MUST always use the html/template // package to generate HTML and not text/template nor raw string concatenation // lest become the victim of cross side scripting (XSS) attacks. package main import ( "bytes" htmltempl "html/template" "io" "net/http" texttempl "text/template" ) func generateHTMLRawConcatUnsafe(imageURL string) string { return bodyPre + `<image src="` + imageURL + `"></image>` + bodyPost } const bodyPre = `<html><head><title>Orijtech Cyber:: Showing XSS pwn from unsafe concatenation</title><body>` const bodyPost = `</body></html>` var genTextTempl = texttempl.Must(texttempl.New("imgp").Parse(bodyPre + `<image src="{{.Src}}"></image>` + bodyPost)) func generateHTMLTextTemplUnSafe(imageURL string) string { buf := new(bytes.Buffer) if err := genTextTempl.Execute(buf, struct{ Src string }{imageURL}); err != nil { panic(err) } return buf.String() } var genHTMLTempl = htmltempl.Must(htmltempl.New("imgp").Parse(bodyPre + `<image src="{{.Src}}"></image>` + bodyPost)) func generateHTMLHTMLTemplSafe(imageURL string) string { buf := new(bytes.Buffer) if err := genHTMLTempl.Execute(buf, struct{ Src string }{imageURL}); err != nil { panic(err) } return buf.String() } func main() { untrustedData := `https://cyber.orijtech.com/orijtech-security-logo+wordmark.svg" onclick="alert('I snuck code in đ´ââ ď¸Pwnedđż!')` mux := http.NewServeMux() mux.HandleFunc("/concat", func(rw http.ResponseWriter, req *http.Request) { io.WriteString(rw, generateHTMLRawConcatUnsafe(untrustedData)) }) mux.HandleFunc("/text", func(rw http.ResponseWriter, req *http.Request) { io.WriteString(rw, generateHTMLTextTemplUnSafe(untrustedData)) }) mux.HandleFunc("/html", func(rw http.ResponseWriter, req *http.Request) { io.WriteString(rw, generateHTMLHTMLTemplSafe(untrustedData)) }) if err := http.ListenAndServe(":8883", mux); err != nil { panic(err) } }
Results:
Using text/template:: pwned
Visiting raw string concatenation:: pwned!
Using html/template:: caught XSS exploit
Remedy:
Use html/template to generate HTML and NOT text/html nor string concatenation to build HTML content.
Iterate on extracted and sorted map keys instead of directly on map iteration which is non-deterministic
For code thatâs supposed to be deterministic, avoid iterating on the map itself and instead use for example golang.org/x/exp/maps.Keys and golang.org/x/exp/slices.Sort. The Go specification allows that map iteration will be in randomized order per https://go.dev/ref/spec#For_statements
reason why this is very important is because:
given the same code and the same sequences, getting entirely different results dependent on an uncontrollable factor like a randomization seed causes lots of problems for systems that are trying to reach consensus
Exhibit of the problem https://go.dev/play/p/-STb7u5QzhN or inlined below
exhibit.go
package main import ( "crypto/sha256" "fmt" ) func iterateAndHash(m map[string]int) []byte { h := sha256.New() for k, v := range m { fmt.Fprintf(h, "%s:%d", k, v) } return h.Sum(nil) } func main() { m := map[string]int{"a": 1, "b": 2, "c": 3, "d": 4, "e": 5, "f": 6, "g": 7, "h": 8, "i": 9, "j": 10, "k": 11, "l": 12} for i := 0; i < 5; i++ { fmt.Printf("Iteration #%-2d: %x\n", i+1, iterateAndHash(m)) } }
which produces different hashes in 5 iterations, yet given the same code
Iteration #1 : 40fd9fe62e7f550849279076cb931d153983cb6fe2775e686b3c35f76df8d629
Iteration #2 : 78681526b89bfb9e9f89cf5d779f5c820f92883edc88699c680a3d1af5825d19
Iteration #3 : f5b1a57dabf1fe02f9d629cd3d70ae387c85c03e4c2087e7a76d74ea62cc1321
Iteration #4 : 3c126d11f061e16e0f8827c3b11e77f1bfa09c380c357d9b3b6e1242f6c29eca
Iteration #5 : 97414e29d6462ac46c6337cd0b5d90aa3523917ae21869c3df0be83bd1b8d7f4
Fix
The correct fix for this problem is to use the extracted keys and sort them
fix.go
package main import ( "crypto/sha256" "fmt" "golang.org/x/exp/maps" "golang.org/x/exp/slices" ) func iterateAndHash(m map[string]int) []byte { h := sha256.New() keys := maps.Keys(m) slices.Sort(keys) for _, k := range keys { fmt.Fprintf(h, "%s:%d", k, m[k]) } return h.Sum(nil) } func main() { m := map[string]int{"a": 1, "b": 2, "c": 3, "d": 4, "e": 5, "f": 6, "g": 7, "h": 8, "i": 9, "j": 10, "k": 11, "l": 12} for i := 0; i < 5; i++ { fmt.Printf("Iteration #%-2d: %x\n", i+1, iterateAndHash(m)) } }
which produces deterministic results
Iteration #1 : f808d40083ca15c3992ed401e97b44243463af852617b5ed7a9f6a9c2aef649a Iteration #2 : f808d40083ca15c3992ed401e97b44243463af852617b5ed7a9f6a9c2aef649a Iteration #3 : f808d40083ca15c3992ed401e97b44243463af852617b5ed7a9f6a9c2aef649a Iteration #4 : f808d40083ca15c3992ed401e97b44243463af852617b5ed7a9f6a9c2aef649a Iteration #5 : f808d40083ca15c3992ed401e97b44243463af852617b5ed7a9f6a9c2aef649a
Enforcement suggestion:
Please use cosmos/gosec which will flag this issue!
Aim for 2+ person code reviews before merge:
The more eyes that look at code, the shallower bugs get, as paraphrased from âLinusâ Lawâ https://en.wikipedia.org/wiki/Linus's_law Getting a second, third and more opinion on code before submission ensures diversity of experiences that can pitch in but also others with different eyes can point out what you might have missed, but even more to build trust. Always strive for 2+ person code reviews.
Avoid rolling out your own algorithms if battled tested libraries exist:
Writing software involves taking ideas and interpreting them by writing code which a computer exists. The cognitive load of writing code means that almost every line of code is subject to misinterpretation unless carefully tested and audited
Binary search is CS-101 whose amazing result is that for sorted sequences, is guaranteed to find a match or not in log2(n) so if n = 1 million, itâll run in 20 steps. It is useful and one of the most studied algorithms that almost everyone can recite in their sleep. Sounds pretty trivial and the algorithmâs description on Wikipedia and as commonly described is:
Binary search compares the target value to the middle element of the array. If they are not equal, the half in which the target cannot lie is eliminated and the search continues on the remaining half, again taking the middle element to compare to the target value, and repeating this until the target value is found. If the search ends with the remaining half being empty, the target is not in the array.
When put into steps:
- For a query
q
in a sorted sequence,seq
, start 2 indices: start=0, end=len(seq)
- while
start
⤠`end`, compute the middle item, mid whose value is the average of start and end
- compare and if seq[mid] == q, return mid
- compare if mid < q, then set start=mid+1
- compare if mid > q, then set end=mid-1
- if we break out of the loop, the item doesnât exist and return
start
or-1
Most folks will go implement the algorithm as is, but there is a subtle bug which is appalling an integer overflow when computing the average mid
naively such as:
mid := (start + end)/2
the result can overflow because the result of (start + end)
can overflow in situations with large lengths of seq
, when that value exceeds the maximum integer value of either (1<<31) - 1
on 32-bit systems or (1<<63) - 1
on 64-bit systems
The fix for this is to make the result of the addition firstly a uint value then performing the division so
mid := int(uint(start + end)/2)
or
mid := int(uint(start + end)>>1)
and NOT
mid := (start + end)/2
Fix:
Please use standard library and well written packages
Avoid rolling out your own cryptography:
Cryptography is a very intricate and highly complex subject; a tip is to find popular and well recommended cryptographic libraries that are battle tested such as from the Go standard library and use them. Cryptography is critical to modern secure communications yet broken cryptography is so much more easier to build, and just subtle broken code can be the difference between security and insecurity. Cryptographers continually attack cryptographic schemes and find novel attacks then retire algorithms, please see for example the CRIME attack present even in well audited and critical infrastructure. There are very subtle/stealthy attacks such as timing attacks that canât be detected except by very highly sophisticated tracing but thatâs virtually impossible to figure out unless you intentionally set up a honey pot to investigate attackers. Use audited libraries that label themselves as constant time. The high attention and maintenance burden of trying to keep up with the latest means that your best defence is relying on well maintained cryptographic libraries.
Test your web server handles in hermetic setups, without having to access the non-local network (internet)
Understanding the behaviour of your systems can be further improved by thorough tests anticipating and asserting on end-to-end results. Most folks donât test their HTTP/web handler logic because they donât know how to wire up web servers nor inputs. It is highly important that tests be diverse and hermetic so that they can be fast, reliable and reproducible. Here is an example of how a server handler can be tested in a hermetic setup
You can learn more about how to tame HTTP in the Go programming language per this article on our engineering blog
Use TLS with your services to avoid MITM attacks plus eavsdropping
Transport Layer Security allows public key cryptography and encryption to protect your data in transit. Gone are the days when SSL certificates cost thousands of dollars per month to have. These days, Letâs Encrypt from the Internet Security Research Group provides free and renewable SSL certificates at scalable and in a variety of languages.
Consensus: avoid time.Now() which is localized and instead use a consensus aware clock
When distributed systems communicate and are trying to get to consensus, theyâll need a common agreed time reference. Using time.Now() subjects systems to serious problems like clock drifts, NTP time attacks where an attacker can control your clock to reject specific proposals or cause chaos, they can cause partitioning in the network because consensus canât be settled upon. This issue manifested as a vulnerability in the Cosmos ecosystem.
Fix:
Please use consensus aware clocks. For Cosmos code, please always us block.HeaderTime
Large distributed systems use highly accurate clocks like in protocols like TrueTime; some open source protocols like RoughTime
Prevent replay attacks of cryptographic objects by using unique identifiers
Replay attacks (of, say, an encrypted payload) can happen in stateless systems, where data received can't be checked to be fresh, and not copied from a previous session. The reuse/lack of a nonce is an attack thatâs basically been done even before the advent of computers.
Use a unique nonce in cryptographic algorithms that require it (AES-GCM, ChaCha20, etc.)
A nonce (number used only once) is a non-secret input to certain ciphers that, if repeated, can annihilate the security of the cipher. This is particularly true of stream cipher-like constructions: AES-CTR, AES-GCM, ChaCha20, Salsa20, and so on. The nonce does not need be random, it only has to be unique (that is, a given nonce must never be reused in combination with the same key). If random nonces are used, then they must be large enough to make the risk of collisions negligible.
Salt your passwords even after hashing them before storing them in databases
Good hash functions compute a digest of information generating hashes with very low collision rates such as with AES; however, if the value âpasswordâ is hashed with a SHA256 cipher, it always produces the value 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
and it is unlikely that other values will produce the same hash aka a collision. If an attacker can collect a listing of one way plain values and their resulting hashes, they can reverse engineer a dictionary of password hashes to plaintext and without some obscurity, it becomes just a problem of collecting a large enough and diverse corpus. Some folks building web applications can be tempted to just store the hash of a password of a value say after hashing them say with MD5 or SHA256. However, unfortunately when hashes are stored, they can be broken by using rainbow tables and dictionary looks which perform pre-saved password hash look-ups, given that. Salting is a technique that adds randomized and fresh data to data thatâll be hashed, but can be authenticated as the original. This highly guarantees that even if everyone shared the same password, the resulting hash wonât be the same, hence it makes it prohibitavely expensive to try to map against rainbow tables and pre-saved hashâplaintext mappings!
Use the latest Go releases & subscribe to the mailing list plus official social media
Goâs security team is very active, hard working and they post up software updates transparently and responsibly disclosing vulnerabilities. Weâve reported a couple of vulnerabilities. Whenever a security release is made, we highly implore that you upgrade immediately.
Do not accept compressed payloads from the world as they can contain vulnerability sequences such as zip bombs
Accepting data compressed from the wild say by customers can be a recipe for disaster. There are vulnerabilities such as zip bombs that canât be mitigated easily unless by careful comparison of the data size of the inputs and memory consumed so far
Donât accept arbitrary inputs for memory allocation: set sane bounds checks and avoid arbitrary integer type casts
When allocating memory such as with primitives like make, ensure that you carefully check user inputs, checking for ranges like even x ⤠0 and x ⼠max. Some inputs naively assume that inputs will be in the range x ⼠0 but really attackers will take advantage of such. During a supply chain audit for cosmos-sdk, we found a curious bug in a popular downstream dependency that could be exploited by passing in a negative value (there wasnât a bounds check for negative value) but later on the code just blindly converted that value into a uint value of which uint(x) where x < 0 can be calculated by maxUint - abs(x) - 1. This vulnerability was reported by us at
The subtlety of that bug is that one can use it to cause âdeath by a thousand cutsâ in which an attacker can stealthy and progressively send many requests of sizes that wonât blow up memory but will cause almost large allocations and eventually due to an overwhelming amount of allocated memory, the server can freeze. In that bug I demonstrated a way to freeze a machine.
Reject arbitrary PIDs being passed into process killers like os.Kill
Weâve found code that implements Command & Control (C&C) systems and innocently it accepted string values via RPC then parsed those values into an integer and sadly didnât perform any range checks then passed the value to os.Kill. Luckily Goâs os.Kill firstly invokes os.FindProcess(PID) which always succeeds then checks the process state. However if that code used a shell to invoke kill
it would be a rain of vulnerabilities.
Use memory safe languages like Go, Rust, Java
Memory safety bugs occur in languages that have no separation between memory layouts and can allow user manipulation of sensitive memory in arbitrary sections of RAM, without any bounds checks. Languages such as C, C++, Assembly variants allow this kind of manipulation and the attack surface area that they expose can even drown out the productivity costs of having gone to bare metal. Modern languages like Go & Rust are usually sufficient & expressive enough to write most programs, please use them. Heavy C++ production code bases like Chromium report that 70% of the bugs theyâve encountered are memory safety bugs https://www.chromium.org/Home/chromium-security/memory-safety/
Conclusion:
Securing systems forever requires attention to detail and intentional concerted effort to prevent vulnerabilities. As long as human beings are involved in writing software, our ideas of expressing and writing software are bound to have weaknesses due to us being human and cognitive dissonance from translating human thoughts to software. However, we can enhance security by leveraging rules from computer systems and using static analyzers, dynamic/runtime tooling and bug checkers.
References
Resource | Comments | URL | |
Go race detector | Just run go test -race | https://go.dev/doc/articles/race_detector | |
Google Chromium 70% of bugs are memory safety bugs | Whenever possible, please use memory safe languages | https://www.chromium.org/Home/chromium-security/memory-safety/ | |
SLSA | https://slsa.dev/ | ||
Chainguard Q3 2022 July 2022, Cosmos Software Supply chain assessment | Please read through this important report to garner new skills & fixes thatâll make a variety of supply chains much more secure | https://drive.google.com/file/d/1BCDUSZ3cSdO8FTD9A-nA21_iViONoFln/view |