Malicious actors have been using rushlink for their evil machinations for a while now. This didn't impact us directly. Recently, google started listing rushlink as an unsafe website. They're not wrong.
To resolve this, we should delete the malicious pastes and prevent people from making new ones.
Proposed solution
The easiest way to do this is with a user system.
Pros:
should be fool-proof
high degree of usability
easy to implement
allows users to view/delete their own pastes
Cons:
collects and stores personal data (username)
requires admin intervention to create new accounts
## Alternatives
A combination of these tools might also be possible.
ssl client certs
Stores less information, but is harder to use
proof-of-work
Loses compatibility with curl and other shortener interfaces
Malicious actors can still do this, although they're less likely to bother
TODO's
check sensitive code (password hashing, user id generation)
improve error handling and reporting
delete user data (username, password) upon deletion
implement paste view and deletion for users
improve code, since I don't know Go
Create a UI
Should FileUploads also have a CreatedBy?
## Problem statement
Malicious actors have been using rushlink for their evil machinations for a while now. This didn't impact us directly. Recently, [google started listing](https://transparencyreport.google.com/safe-browsing/search?url=https:%2F%2Fhashru.link%2F) rushlink as an unsafe website. They're not wrong.
To resolve this, we should delete the malicious pastes and prevent people from making new ones.
## Proposed solution
The easiest way to do this is with a user system.
**Pros:**
- should be fool-proof
- high degree of usability
- easy to implement
- allows users to view/delete their own pastes
**Cons:**
- collects and stores personal data (username)
- requires admin intervention to create new accounts
<details><summary>
## Alternatives
</summary>
A combination of these tools might also be possible.
### ssl client certs
- Stores less information, but is harder to use
### proof-of-work
- Loses compatibility with curl and other shortener interfaces
- Malicious actors can still do this, although they're less likely to bother
</details>
## TODO's
- [ ] check sensitive code (password hashing, user id generation)
- [ ] improve error handling and reporting
- [ ] delete user data (username, password) upon deletion
- [ ] implement paste view and deletion for users
- [ ] improve code, since I don't know Go
- [ ] Create a UI
- [ ] Should FileUploads also have a CreatedBy?
This could be correct, but it's not exactly readable. It's not clear what canAlsoBe means in this context, and what kind of values it should represent.
It's perhaps better to add a function like:
func(rl*rushlink)setWWWAuthenticate(whttp.ResponseWriter,r*http.Request){// Set authentication headers for Basic Authenticationw.Header().Set("WWW-Authenticate",`Basic realm="Restricted"`)w.WriteHeader(http.StatusUnauthorized)}
and then implement the logic for authenticating the user
user,err:=db.Authenticate(rl.db,username,password)iferr!=nil{log.Printf("authentication failure: %v",err)rl.setWWWAuthenticate(w,r)returnnil}ifshouldBeAdmin&&!user.Admin{log.Printf("user should be admin, but isn't: %v",err)rl.setWWWAuthenticate(w,r)returnnil}// TODO: canAlsoBe?// User has been authenticatedreturnuser
This could be correct, but it's not exactly readable. It's not clear what `canAlsoBe` means in this context, and what kind of values it should represent.
It's perhaps better to add a function like:
```go
func (rl *rushlink) setWWWAuthenticate(w http.ResponseWriter, r *http.Request) {
// Set authentication headers for Basic Authentication
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(http.StatusUnauthorized)
}
```
and then implement the logic for authenticating the user
```go
user, err := db.Authenticate(rl.db, username, password)
if err != nil {
log.Printf("authentication failure: %v", err)
rl.setWWWAuthenticate(w, r)
return nil
}
if shouldBeAdmin && !user.Admin {
log.Printf("user should be admin, but isn't: %v", err)
rl.setWWWAuthenticate(w, r)
return nil
}
// TODO: canAlsoBe?
// User has been authenticated
return user
```
(nit: also define the 2, 64*1024, 1 in the var block above, such that you can reuse it in comparePassword; alternatively, create a function createHashFromPasswordAndSalt(password, salt []byte) []byte that does this for you... also specifying why you chose these parameters)
(nit: also define the `2`, `64*1024`, `1` in the `var` block above, such that you can reuse it in `comparePassword`; alternatively, create a function `createHashFromPasswordAndSalt(password, salt []byte) []byte` that does this for you... also specifying why you chose these parameters)
According to the PHC format specification, padding characters are omitted. base64.URLEncoding includes padding characters. Use base64.RawURLEncoding instead.
(Technically speaking, you should use base64.RawStdEncoding, because that follows the exact alphabet as the specification. If you decide that base64.URLEncoding (or base64.RawURLEncoding) also works, then make it clear in the comment that you deviate from the specification).
According to the [PHC format specification](https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#b64), padding characters are omitted. `base64.URLEncoding` includes padding characters. Use `base64.RawURLEncoding` instead.
(Technically speaking, you should use `base64.RawStdEncoding`, because that follows the exact alphabet as the specification. If you decide that `base64.URLEncoding` (or `base64.RawURLEncoding`) also works, then make it clear in the comment that you deviate from the specification).
Is there a specific reason you omitted the (optional) field version? The argon2 package defines one, so it seems a good idea to include that as well.
Is there a specific reason you omitted the (optional) field `version`? The `argon2` package [defines one](https://pkg.go.dev/golang.org/x/crypto/argon2#Version), so it seems a good idea to include that as well.
It's not safe to do a slice expression that requires at least two returned elements on a function that may return an empty slice, or a slice of length one.
It's not safe to do a slice expression that requires at least two returned elements on a function that may return an empty slice, or a slice of length one.
(another option: add var errInvalidDBPasswordFormat = errors.New("invalid password format in db") in the global scope, and return that here. Works nice with `errors.Is(err, errInvalidDBPasswordFormat), as I suppose we want to log such occurrences)
(another option: add `var errInvalidDBPasswordFormat = errors.New("invalid password format in db")` in the global scope, and return that here. Works nice with `errors.Is(err, errInvalidDBPasswordFormat), as I suppose we want to log such occurrences)
I find it somewhat risky that this function may update the admin status. I would prefer a SetUserAdminStatus(db *gorm.DB, username string, isAdmin bool) error that only changes that bit. It also simplifies the code a bit that handles changing the user.
I find it somewhat risky that this function may update the admin status. I would prefer a `SetUserAdminStatus(db *gorm.DB, username string, isAdmin bool) error` that only changes that bit. It also simplifies the code a bit that handles changing the user.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem statement
Malicious actors have been using rushlink for their evil machinations for a while now. This didn't impact us directly. Recently, google started listing rushlink as an unsafe website. They're not wrong.
To resolve this, we should delete the malicious pastes and prevent people from making new ones.
Proposed solution
The easiest way to do this is with a user system.
Pros:
Cons:
## Alternatives
A combination of these tools might also be possible.
ssl client certs
proof-of-work
TODO's
@@ -32,6 +32,10 @@ func main() {log.Fatal(errors.Wrap(err, "migrating database"))}if err := db.CreateAdminUser(database, "admin"); err != nil {I would be fine with a command line option that allows the operator to create users.
@@ -33,2 +33,4 @@}if err := db.CreateAdminUser(database, "admin"); err != nil {log.Fatalln(err)(not
go fmted)@@ -276,0 +294,4 @@return}}func (rl *rushlink) createUserHandler(w http.ResponseWriter, r *http.Request) {(I would be fine with leaving this out of the HTTP part)
@@ -278,0 +309,4 @@new_username := r.FormValue("username")new_password := r.FormValue("password")new_admin, _ := strconv.ParseBool(r.FormValue("admin"))Missing
errcheck@@ -278,0 +334,4 @@// Authenticate the useruser, err := db.Authenticate(rl.db, username, password)if err != nil || (shouldBeAdmin && !user.Admin && (canAlsoBe == nil || *canAlsoBe != user.User)) {This could be correct, but it's not exactly readable. It's not clear what
canAlsoBemeans in this context, and what kind of values it should represent.It's perhaps better to add a function like:
and then implement the logic for authenticating the user
@@ -39,2 +39,4 @@PubID uuid.UUID `gorm:"uniqueIndex"`// User ID that created this fileCreatedBy uint `gorm:"index"`(not
go fmted)@@ -0,0 +66,4 @@return &user, nil}const ((I would put these password-related
constandfuncs in a separate file, because they do nothing with regards to users)@@ -0,0 +81,4 @@}// Hash the password using argon2idhash := argon2.IDKey([]byte(password), salt, 2, 64*1024, 1, pwdHashSize)(nit: also define the
2,64*1024,1in thevarblock above, such that you can reuse it incomparePassword; alternatively, create a functioncreateHashFromPasswordAndSalt(password, salt []byte) []bytethat does this for you... also specifying why you chose these parameters)@@ -0,0 +84,4 @@hash := argon2.IDKey([]byte(password), salt, 2, 64*1024, 1, pwdHashSize)// Encode the salt and hash as a string in PHC formatencodedSalt := base64.URLEncoding.EncodeToString(salt)According to the PHC format specification, padding characters are omitted.
base64.URLEncodingincludes padding characters. Usebase64.RawURLEncodinginstead.(Technically speaking, you should use
base64.RawStdEncoding, because that follows the exact alphabet as the specification. If you decide thatbase64.URLEncoding(orbase64.RawURLEncoding) also works, then make it clear in the comment that you deviate from the specification).@@ -0,0 +86,4 @@// Encode the salt and hash as a string in PHC formatencodedSalt := base64.URLEncoding.EncodeToString(salt)encodedHash := base64.URLEncoding.EncodeToString(hash)return fmt.Sprintf("$%s$%s$%s$%s", pwdAlgo, pwdParams, encodedSalt, encodedHash), nilIs there a specific reason you omitted the (optional) field
version? Theargon2package defines one, so it seems a good idea to include that as well.@@ -0,0 +91,4 @@func comparePassword(hashedPassword string, password string) (bool, error) {// Extract the salt and hash from the hashed password stringfields := strings.Split(hashedPassword, "$")[1:]It's not safe to do a slice expression that requires at least two returned elements on a function that may return an empty slice, or a slice of length one.
@@ -0,0 +93,4 @@// Extract the salt and hash from the hashed password stringfields := strings.Split(hashedPassword, "$")[1:]if len(fields) != 4 || fields[0] != pwdAlgo || fields[1] != pwdParams {return false, errors.New("invalid password format in db")(another option: add
var errInvalidDBPasswordFormat = errors.New("invalid password format in db")in the global scope, and return that here. Works nice with `errors.Is(err, errInvalidDBPasswordFormat), as I suppose we want to log such occurrences)@@ -0,0 +98,4 @@encodedSalt, encodedHash := fields[2], fields[3]// Decode the salt and hash from base64salt, err := base64.URLEncoding.DecodeString(encodedSalt)(See comment above on
RawURLEncoding)@@ -0,0 +102,4 @@if err != nil {return false, err}hash, err := base64.URLEncoding.DecodeString(encodedHash)(Idem)
@@ -0,0 +112,4 @@// Compare the computed hash with the stored hash// todo constant time?return bytes.Equal(hash, computedHash), nilreturn subtle.ConstantTimeCompare(hash, computedHash) == 1, nil(fromcrypto/subtle)@@ -0,0 +141,4 @@// Update the user fieldsexistingUser.User = updatedUser.UserexistingUser.Password = updatedUser.PasswordexistingUser.Admin = updatedUser.AdminI find it somewhat risky that this function may update the admin status. I would prefer a
SetUserAdminStatus(db *gorm.DB, username string, isAdmin bool) errorthat only changes that bit. It also simplifies the code a bit that handles changing the user.e8fdc75183to0643176ed1588725b235to44e74b5d0cView command line instructions
Checkout
From your project repository, check out a new branch and test the changes.