komuw / ong

ong, is a Go http toolkit.
MIT License
16 stars 4 forks source link

add rate limiter/blocker for bots #96

Open komuw opened 1 year ago

komuw commented 1 year ago

One way to do that is to;

Maybe integrate it with the csrf middleware?

komuw commented 1 year ago

related: https://github.com/komuw/ong/issues/95

komuw commented 1 year ago

Do note that some pple may be using password managers and thus are very fast in filling out a form, especially short forms like login screens.

komuw commented 1 year ago

Note that bots can still circumvent this by changing the timestamp(before submission) to add a delay. However according the following comment, they rarely do; and even if they were to do so, u can use a crypto nonce:

I'm already using this timestamp technique on my website and so far no bot operator has bothered 
trying to work around this. However even if some bot operator were to specifically target a 
website using this technique and try to decrease the timestamp, 
I believe you could still force a bot to wait by just changing the website to use something 
like a cryptographic nonce that includes a timestamp instead of just a simple timestamp that 
can be understood easily.

https://news.ycombinator.com/item?id=32347438

komuw commented 1 year ago

If we ever do this, we should not use 4seconds, it is too large. We need to maybe use milliseconds in order not to block legitimate users who are too fast or use password managers.

komuw commented 1 year ago
package main

import (
    cryptoRand "crypto/rand"
    "fmt"
    mathRand "math/rand"
    "time"

    "golang.org/x/exp/slices"
)

// Most of the code here is insipired by(or taken from):
//   (a) https://github.com/gorilla/csrf whose license(BSD 3-Clause "New") can be found here: https://github.com/gorilla/csrf/blob/v1.7.1/LICENSE
//

// var tokenLength int

// func init() {
//  mathRand.Seed(time.Now().UTC().UnixNano())
//  tokenLength = mathRand.Intn(20) + 12 // plus 12 to guarantee that we do not get zero, or a vert small number
// }

var tokenLength = 15 // equal to len of bytes generated by time.MarshalBinary()

func generateRandomBytes(n int) []byte {
    b := make([]byte, n)
    if _, err := cryptoRand.Read(b); err != nil {
        b = make([]byte, n)
        mathRand.Seed(time.Now().UTC().UnixNano())
        _, _ = mathRand.Read(b)
    }

    return b
}

func mask(realToken []byte) []byte {
    otp := generateRandomBytes(len(realToken))

    // XOR the OTP with the real token to generate a masked token. Append the
    // OTP to the front of the masked token to allow unmasking in the subsequent
    // request.
    return append(otp, xorToken(otp, realToken)...)
}

// xorToken XORs tokens ([]byte) to provide unique-per-request CSRF tokens. It
// will return a masked token if the base token is XOR'ed with a one-time-pad.
// An unmasked token will be returned if a masked token is XOR'ed with the
// one-time-pad used to mask it.
func xorToken(a, b []byte) []byte {
    n := len(a)
    if len(b) < n {
        n = len(b)
    }

    res := make([]byte, n)

    for i := 0; i < n; i++ {
        res[i] = a[i] ^ b[i]
    }

    return res
}

// unmask splits the issued token (one-time-pad + masked token) and returns the
// unmasked request token for comparison.
func unmask(issuedToken []byte) []byte {
    // Issued tokens are always masked and combined with the pad.
    if len(issuedToken) != tokenLength*2 {
        return nil
    }

    // We now know the length of the byte slice.
    otp := issuedToken[tokenLength:]
    masked := issuedToken[:tokenLength]

    // Unmask the token by XOR'ing it against the OTP used to mask it.
    return xorToken(otp, masked)
}

func main() {
    realTimeToken, timeThen := getTime()
    fmt.Println("realTimeToken: ", realTimeToken, len(realTimeToken))
    fmt.Println("timeThen: ", timeThen)

    maskedTimeToken := mask(realTimeToken)
    fmt.Println("maskedTimeToken: ", maskedTimeToken)

    unmaskedTimeToken := unmask(maskedTimeToken)
    fmt.Println("unmaskedTimeToken: ", unmaskedTimeToken)
    if !slices.Equal(realTimeToken, unmaskedTimeToken) {
        panic("real != unmasked")
    }

    time.Sleep(45 * time.Second)
    timeAfter := time.Now()
    e := timeAfter.UnmarshalBinary(unmaskedTimeToken)
    if e != nil {
        panic(e)
    }
    fmt.Println("timeAfter: ", timeAfter)

    if !timeAfter.Equal(timeThen) {
        panic("time are not equal")
    }
}

func getTime() ([]byte, time.Time) {
    now := time.Now().UTC()
    b, err := now.MarshalBinary()
    if err != nil {
        panic("unable to time.MarshalBinary")
    }
    return b, now
}

https://go.dev/play/p/nyVvONqQKyf

komuw commented 1 year ago

or do this instead; https://news.ycombinator.com/item?id=35272685

for(i=0; i<INT_MAX; i++) {
      output = hash(concat(server_provided_data, i));
      if(check_output(output))
        break;