diff --git a/.golangci.yml b/.golangci.yml
index 22897ef9..ca9c66df 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -40,3 +40,10 @@ linters-settings:
 
   misspell:
     locale: US
+issues:
+  exclude-rules:
+    - path: toxics/corrupt\.go
+      linters:
+        - gosec
+      # we don't need cryptographically secure RNGs for this
+      text: "G404:"
diff --git a/README.md b/README.md
index b8bf9a76..0a2f2e16 100644
--- a/README.md
+++ b/README.md
@@ -446,6 +446,12 @@ Closes connection when transmitted data exceeded limit.
 
  - `bytes`: number of bytes it should transmit before connection is closed
 
+#### corrupt
+
+Flips random bits of the input stream, corrupting it.
+
+ - `probability`: probability of any given bit in the input of being flipped
+
 ### HTTP API
 
 All communication with the Toxiproxy daemon from the client happens through the
diff --git a/scripts/test-e2e b/scripts/test-e2e
index 42dafba5..f149ef86 100755
--- a/scripts/test-e2e
+++ b/scripts/test-e2e
@@ -169,6 +169,18 @@ cli toxic delete --toxicName="reset_peer" shopify_http
 
 echo -e "-----------------\n"
 
+echo "=== Corrupt toxic"
+
+cli toxic add --type=corrupt \
+              --toxicName="corrupt" \
+              --attribute="probability=1.0" \
+              --toxicity=1.0 \
+              shopify_http
+cli inspect shopify_http
+cli toxic delete --toxicName="corrupt" shopify_http
+
+echo -e "-----------------\n"
+
 echo "== Metrics test"
 wait_for_url http://localhost:20000/test1
 curl -s http://localhost:8474/metrics | grep -E '^toxiproxy_proxy_sent_bytes_total{direction="downstream",listener="127.0.0.1:20000",proxy="shopify_http",upstream="localhost:20002"} [0-9]+'
diff --git a/toxics/corrupt.go b/toxics/corrupt.go
new file mode 100644
index 00000000..58f5e4ee
--- /dev/null
+++ b/toxics/corrupt.go
@@ -0,0 +1,70 @@
+package toxics
+
+import (
+	"io"
+	"math/rand"
+
+	"github.com/Shopify/toxiproxy/v2/stream"
+)
+
+type CorruptToxic struct {
+	// probability of bit flips
+	Prob float64 `json:"probability"`
+}
+
+// reference: https://stackoverflow.com/a/2076028/2708711
+func generate_mask(num_bytes int, prob float64, gas int) []byte {
+	tol := 0.001
+	x := make([]byte, num_bytes)
+	rand.Read(x)
+	if gas <= 0 {
+		return x
+	}
+	if prob > 0.5+tol {
+		y := generate_mask(num_bytes, 2*prob-1, gas-1)
+		for i := 0; i < num_bytes; i++ {
+			x[i] |= y[i]
+		}
+		return x
+	}
+	if prob < 0.5-tol {
+		y := generate_mask(num_bytes, 2*prob, gas-1)
+		for i := 0; i < num_bytes; i++ {
+			x[i] &= y[i]
+		}
+		return x
+	}
+	return x
+}
+
+func (t *CorruptToxic) corrupt(data []byte) {
+	gas := 10
+	mask := generate_mask(len(data), t.Prob, gas)
+	for i := 0; i < len(data); i++ {
+		data[i] ^= mask[i]
+	}
+}
+
+func (t *CorruptToxic) Pipe(stub *ToxicStub) {
+	buf := make([]byte, 32*1024)
+	writer := stream.NewChanWriter(stub.Output)
+	reader := stream.NewChanReader(stub.Input)
+	reader.SetInterrupt(stub.Interrupt)
+	for {
+		n, err := reader.Read(buf)
+		if err == stream.ErrInterrupted {
+			t.corrupt(buf[:n])
+			writer.Write(buf[:n])
+			return
+		} else if err == io.EOF {
+			stub.Close()
+			return
+		}
+		t.corrupt(buf[:n])
+		writer.Write(buf[:n])
+	}
+}
+
+func init() {
+	Register("corrupt", new(CorruptToxic))
+}
diff --git a/toxics/corrupt_test.go b/toxics/corrupt_test.go
new file mode 100644
index 00000000..b1830272
--- /dev/null
+++ b/toxics/corrupt_test.go
@@ -0,0 +1,77 @@
+package toxics_test
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/Shopify/toxiproxy/v2/stream"
+	"github.com/Shopify/toxiproxy/v2/toxics"
+)
+
+func count_flips(before, after []byte) int {
+	res := 0
+	for i := 0; i < len(before); i++ {
+		if before[i] != after[i] {
+			res += 1
+		}
+	}
+	return res
+}
+
+func DoCorruptEcho(corrupt *toxics.CorruptToxic) ([]byte, []byte) {
+	len_data := 100
+	data0 := []byte(strings.Repeat("a", len_data))
+	data1 := make([]byte, len_data)
+	copy(data1, data0)
+
+	input := make(chan *stream.StreamChunk)
+	output := make(chan *stream.StreamChunk)
+	stub := toxics.NewToxicStub(input, output)
+
+	done := make(chan bool)
+	go func() {
+		corrupt.Pipe(stub)
+		done <- true
+	}()
+	defer func() {
+		close(input)
+		for {
+			select {
+			case <-done:
+				return
+			case <-output:
+			}
+		}
+	}()
+
+	input <- &stream.StreamChunk{Data: data1}
+
+	result := <-output
+	return data0, result.Data
+}
+
+func TestCorruptToxicLowProb(t *testing.T) {
+	corrupt := &toxics.CorruptToxic{Prob: 0.001}
+	original, corrupted := DoCorruptEcho(corrupt)
+
+	num_flips := count_flips(original, corrupted)
+
+	tolerance := 5
+	expected := 0
+	if num_flips > expected+tolerance {
+		t.Errorf("Too many bytes flipped! (note: this test has a very low false positive probability)")
+	}
+}
+
+func TestCorruptToxicHighProb(t *testing.T) {
+	corrupt := &toxics.CorruptToxic{Prob: 0.999}
+	original, corrupted := DoCorruptEcho(corrupt)
+
+	num_flips := count_flips(original, corrupted)
+
+	tolerance := 5
+	expected := 100
+	if num_flips < expected-tolerance {
+		t.Errorf("Too few bytes flipped! (note: this test has a very low false positive probability)")
+	}
+}