Save initial signup information for users to aid in spam prevention (#31852)

This will allow instance admins to view signup pattern patterns for
public instances. It is modelled after discourse, mastodon, and
MediaWiki's approaches.

Note: This has privacy implications, but as the above-stated open-source
projects take this approach, especially MediaWiki, which I have no doubt
looked into this thoroughly, it is likely okay for us, too. However, I
would be appreciative of any feedback on how this could be improved.

---------

Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
techknowlogick 2024-09-09 17:05:16 -04:00 committed by GitHub
parent a323a82ec4
commit f183783baa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 61 additions and 23 deletions

View File

@ -158,7 +158,7 @@ func runCreateUser(c *cli.Context) error {
IsRestricted: restricted, IsRestricted: restricted,
} }
if err := user_model.CreateUser(ctx, u, overwriteDefault); err != nil { if err := user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
return fmt.Errorf("CreateUser: %w", err) return fmt.Errorf("CreateUser: %w", err)
} }

View File

@ -507,6 +507,9 @@ INTERNAL_TOKEN =
;; stemming from cached/logged plain-text API tokens. ;; stemming from cached/logged plain-text API tokens.
;; In future releases, this will become the default behavior ;; In future releases, this will become the default behavior
;DISABLE_QUERY_AUTH_TOKEN = false ;DISABLE_QUERY_AUTH_TOKEN = false
;;
;; On user registration, record the IP address and user agent of the user to help identify potential abuse.
;; RECORD_USER_SIGNUP_METADATA = false
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@ -14,4 +14,8 @@ const (
UserActivityPubPrivPem = "activitypub.priv_pem" UserActivityPubPrivPem = "activitypub.priv_pem"
// UserActivityPubPubPem is user's public key // UserActivityPubPubPem is user's public key
UserActivityPubPubPem = "activitypub.pub_pem" UserActivityPubPubPem = "activitypub.pub_pem"
// SignupIP is the IP address that the user signed up with
SignupIP = "signup.ip"
// SignupUserAgent is the user agent that the user signed up with
SignupUserAgent = "signup.user_agent"
) )

View File

@ -150,6 +150,14 @@ type User struct {
KeepActivityPrivate bool `xorm:"NOT NULL DEFAULT false"` KeepActivityPrivate bool `xorm:"NOT NULL DEFAULT false"`
} }
// Meta defines the meta information of a user, to be stored in the K/V table
type Meta struct {
// Store the initial registration of the user, to aid in spam prevention
// Ensure that one IP isn't creating many accounts (following mediawiki approach)
InitialIP string
InitialUserAgent string
}
func init() { func init() {
db.RegisterModel(new(User)) db.RegisterModel(new(User))
} }
@ -615,17 +623,17 @@ type CreateUserOverwriteOptions struct {
} }
// CreateUser creates record of a new user. // CreateUser creates record of a new user.
func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) { func CreateUser(ctx context.Context, u *User, meta *Meta, overwriteDefault ...*CreateUserOverwriteOptions) (err error) {
return createUser(ctx, u, false, overwriteDefault...) return createUser(ctx, u, meta, false, overwriteDefault...)
} }
// AdminCreateUser is used by admins to manually create users // AdminCreateUser is used by admins to manually create users
func AdminCreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) { func AdminCreateUser(ctx context.Context, u *User, meta *Meta, overwriteDefault ...*CreateUserOverwriteOptions) (err error) {
return createUser(ctx, u, true, overwriteDefault...) return createUser(ctx, u, meta, true, overwriteDefault...)
} }
// createUser creates record of a new user. // createUser creates record of a new user.
func createUser(ctx context.Context, u *User, createdByAdmin bool, overwriteDefault ...*CreateUserOverwriteOptions) (err error) { func createUser(ctx context.Context, u *User, meta *Meta, createdByAdmin bool, overwriteDefault ...*CreateUserOverwriteOptions) (err error) {
if err = IsUsableUsername(u.Name); err != nil { if err = IsUsableUsername(u.Name); err != nil {
return err return err
} }
@ -745,6 +753,22 @@ func createUser(ctx context.Context, u *User, createdByAdmin bool, overwriteDefa
return err return err
} }
if setting.RecordUserSignupMetadata {
// insert initial IP and UserAgent
if err = SetUserSetting(ctx, u.ID, SignupIP, meta.InitialIP); err != nil {
return err
}
// trim user agent string to a reasonable length, if necessary
userAgent := strings.TrimSpace(meta.InitialUserAgent)
if len(userAgent) > 255 {
userAgent = userAgent[:255]
}
if err = SetUserSetting(ctx, u.ID, SignupUserAgent, userAgent); err != nil {
return err
}
}
// insert email address // insert email address
if err := db.Insert(ctx, &EmailAddress{ if err := db.Insert(ctx, &EmailAddress{
UID: u.ID, UID: u.ID,

View File

@ -227,7 +227,7 @@ func TestCreateUserInvalidEmail(t *testing.T) {
MustChangePassword: false, MustChangePassword: false,
} }
err := user_model.CreateUser(db.DefaultContext, user) err := user_model.CreateUser(db.DefaultContext, user, &user_model.Meta{})
assert.Error(t, err) assert.Error(t, err)
assert.True(t, user_model.IsErrEmailCharIsNotSupported(err)) assert.True(t, user_model.IsErrEmailCharIsNotSupported(err))
} }
@ -241,7 +241,7 @@ func TestCreateUserEmailAlreadyUsed(t *testing.T) {
user.Name = "testuser" user.Name = "testuser"
user.LowerName = strings.ToLower(user.Name) user.LowerName = strings.ToLower(user.Name)
user.ID = 0 user.ID = 0
err := user_model.CreateUser(db.DefaultContext, user) err := user_model.CreateUser(db.DefaultContext, user, &user_model.Meta{})
assert.Error(t, err) assert.Error(t, err)
assert.True(t, user_model.IsErrEmailAlreadyUsed(err)) assert.True(t, user_model.IsErrEmailAlreadyUsed(err))
} }
@ -258,7 +258,7 @@ func TestCreateUserCustomTimestamps(t *testing.T) {
user.ID = 0 user.ID = 0
user.Email = "unique@example.com" user.Email = "unique@example.com"
user.CreatedUnix = creationTimestamp user.CreatedUnix = creationTimestamp
err := user_model.CreateUser(db.DefaultContext, user) err := user_model.CreateUser(db.DefaultContext, user, &user_model.Meta{})
assert.NoError(t, err) assert.NoError(t, err)
fetched, err := user_model.GetUserByID(context.Background(), user.ID) fetched, err := user_model.GetUserByID(context.Background(), user.ID)
@ -283,7 +283,7 @@ func TestCreateUserWithoutCustomTimestamps(t *testing.T) {
user.Email = "unique@example.com" user.Email = "unique@example.com"
user.CreatedUnix = 0 user.CreatedUnix = 0
user.UpdatedUnix = 0 user.UpdatedUnix = 0
err := user_model.CreateUser(db.DefaultContext, user) err := user_model.CreateUser(db.DefaultContext, user, &user_model.Meta{})
assert.NoError(t, err) assert.NoError(t, err)
timestampEnd := time.Now().Unix() timestampEnd := time.Now().Unix()

View File

@ -37,6 +37,7 @@ var (
DisableQueryAuthToken bool DisableQueryAuthToken bool
CSRFCookieName = "_csrf" CSRFCookieName = "_csrf"
CSRFCookieHTTPOnly = true CSRFCookieHTTPOnly = true
RecordUserSignupMetadata = false
) )
// loadSecret load the secret from ini by uriKey or verbatimKey, only one of them could be set // loadSecret load the secret from ini by uriKey or verbatimKey, only one of them could be set
@ -164,6 +165,8 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
// TODO: default value should be true in future releases // TODO: default value should be true in future releases
DisableQueryAuthToken = sec.Key("DISABLE_QUERY_AUTH_TOKEN").MustBool(false) DisableQueryAuthToken = sec.Key("DISABLE_QUERY_AUTH_TOKEN").MustBool(false)
RecordUserSignupMetadata = sec.Key("RECORD_USER_SIGNUP_METADATA").MustBool(false)
// warn if the setting is set to false explicitly // warn if the setting is set to false explicitly
if sectionHasDisableQueryAuthToken && !DisableQueryAuthToken { if sectionHasDisableQueryAuthToken && !DisableQueryAuthToken {
log.Warn("Enabling Query API Auth tokens is not recommended. DISABLE_QUERY_AUTH_TOKEN will default to true in gitea 1.23 and will be removed in gitea 1.24.") log.Warn("Enabling Query API Auth tokens is not recommended. DISABLE_QUERY_AUTH_TOKEN will default to true in gitea 1.23 and will be removed in gitea 1.24.")

View File

@ -133,7 +133,7 @@ func CreateUser(ctx *context.APIContext) {
u.UpdatedUnix = u.CreatedUnix u.UpdatedUnix = u.CreatedUnix
} }
if err := user_model.AdminCreateUser(ctx, u, overwriteDefault); err != nil { if err := user_model.AdminCreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
if user_model.IsErrUserAlreadyExist(err) || if user_model.IsErrUserAlreadyExist(err) ||
user_model.IsErrEmailAlreadyUsed(err) || user_model.IsErrEmailAlreadyUsed(err) ||
db.IsErrNameReserved(err) || db.IsErrNameReserved(err) ||

View File

@ -554,7 +554,7 @@ func SubmitInstall(ctx *context.Context) {
IsActive: optional.Some(true), IsActive: optional.Some(true),
} }
if err = user_model.CreateUser(ctx, u, overwriteDefault); err != nil { if err = user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
if !user_model.IsErrUserAlreadyExist(err) { if !user_model.IsErrUserAlreadyExist(err) {
setting.InstallLock = false setting.InstallLock = false
ctx.Data["Err_AdminName"] = true ctx.Data["Err_AdminName"] = true

View File

@ -177,7 +177,7 @@ func NewUserPost(ctx *context.Context) {
u.MustChangePassword = form.MustChangePassword u.MustChangePassword = form.MustChangePassword
} }
if err := user_model.AdminCreateUser(ctx, u, overwriteDefault); err != nil { if err := user_model.AdminCreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
switch { switch {
case user_model.IsErrUserAlreadyExist(err): case user_model.IsErrUserAlreadyExist(err):
ctx.Data["Err_UserName"] = true ctx.Data["Err_UserName"] = true

View File

@ -541,7 +541,11 @@ func createAndHandleCreatedUser(ctx *context.Context, tpl base.TplName, form any
// createUserInContext creates a user and handles errors within a given context. // createUserInContext creates a user and handles errors within a given context.
// Optionally a template can be specified. // Optionally a template can be specified.
func createUserInContext(ctx *context.Context, tpl base.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, gothUser *goth.User, allowLink bool) (ok bool) { func createUserInContext(ctx *context.Context, tpl base.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, gothUser *goth.User, allowLink bool) (ok bool) {
if err := user_model.CreateUser(ctx, u, overwrites); err != nil { meta := &user_model.Meta{
InitialIP: ctx.RemoteAddr(),
InitialUserAgent: ctx.Req.UserAgent(),
}
if err := user_model.CreateUser(ctx, u, meta, overwrites); err != nil {
if allowLink && (user_model.IsErrUserAlreadyExist(err) || user_model.IsErrEmailAlreadyUsed(err)) { if allowLink && (user_model.IsErrUserAlreadyExist(err) || user_model.IsErrEmailAlreadyUsed(err)) {
if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingAuto { if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingAuto {
var user *user_model.User var user *user_model.User

View File

@ -164,7 +164,7 @@ func (r *ReverseProxy) newUser(req *http.Request) *user_model.User {
IsActive: optional.Some(true), IsActive: optional.Some(true),
} }
if err := user_model.CreateUser(req.Context(), user, &overwriteDefault); err != nil { if err := user_model.CreateUser(req.Context(), user, &user_model.Meta{}, &overwriteDefault); err != nil {
// FIXME: should I create a system notice? // FIXME: should I create a system notice?
log.Error("CreateUser: %v", err) log.Error("CreateUser: %v", err)
return nil return nil

View File

@ -89,7 +89,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
IsActive: optional.Some(true), IsActive: optional.Some(true),
} }
err := user_model.CreateUser(ctx, user, overwriteDefault) err := user_model.CreateUser(ctx, user, &user_model.Meta{}, overwriteDefault)
if err != nil { if err != nil {
return user, err return user, err
} }

View File

@ -129,7 +129,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
IsActive: optional.Some(true), IsActive: optional.Some(true),
} }
err = user_model.CreateUser(ctx, usr, overwriteDefault) err = user_model.CreateUser(ctx, usr, &user_model.Meta{}, overwriteDefault)
if err != nil { if err != nil {
log.Error("SyncExternalUsers[%s]: Error creating user %s: %v", source.authSource.Name, su.Username, err) log.Error("SyncExternalUsers[%s]: Error creating user %s: %v", source.authSource.Name, su.Username, err)
} }

View File

@ -36,7 +36,7 @@ func TestSource(t *testing.T) {
Email: "external@example.com", Email: "external@example.com",
} }
err := user_model.CreateUser(context.Background(), user, &user_model.CreateUserOverwriteOptions{}) err := user_model.CreateUser(context.Background(), user, &user_model.Meta{}, &user_model.CreateUserOverwriteOptions{})
assert.NoError(t, err) assert.NoError(t, err)
e := &user_model.ExternalLoginUser{ e := &user_model.ExternalLoginUser{

View File

@ -63,7 +63,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
IsActive: optional.Some(true), IsActive: optional.Some(true),
} }
if err := user_model.CreateUser(ctx, user, overwriteDefault); err != nil { if err := user_model.CreateUser(ctx, user, &user_model.Meta{}, overwriteDefault); err != nil {
return user, err return user, err
} }

View File

@ -79,7 +79,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
IsActive: optional.Some(true), IsActive: optional.Some(true),
} }
if err := user_model.CreateUser(ctx, user, overwriteDefault); err != nil { if err := user_model.CreateUser(ctx, user, &user_model.Meta{}, overwriteDefault); err != nil {
return user, err return user, err
} }

View File

@ -176,7 +176,7 @@ func (s *SSPI) newUser(ctx context.Context, username string, cfg *sspi.Source) (
KeepEmailPrivate: optional.Some(true), KeepEmailPrivate: optional.Some(true),
EmailNotificationsPreference: &emailNotificationPreference, EmailNotificationsPreference: &emailNotificationPreference,
} }
if err := user_model.CreateUser(ctx, user, overwriteDefault); err != nil { if err := user_model.CreateUser(ctx, user, &user_model.Meta{}, overwriteDefault); err != nil {
return nil, err return nil, err
} }

View File

@ -92,7 +92,7 @@ func TestCreateUser(t *testing.T) {
MustChangePassword: false, MustChangePassword: false,
} }
assert.NoError(t, user_model.CreateUser(db.DefaultContext, user)) assert.NoError(t, user_model.CreateUser(db.DefaultContext, user, &user_model.Meta{}))
assert.NoError(t, DeleteUser(db.DefaultContext, user, false)) assert.NoError(t, DeleteUser(db.DefaultContext, user, false))
} }
@ -177,7 +177,7 @@ func TestCreateUser_Issue5882(t *testing.T) {
for _, v := range tt { for _, v := range tt {
setting.Admin.DisableRegularOrgCreation = v.disableOrgCreation setting.Admin.DisableRegularOrgCreation = v.disableOrgCreation
assert.NoError(t, user_model.CreateUser(db.DefaultContext, v.user)) assert.NoError(t, user_model.CreateUser(db.DefaultContext, v.user, &user_model.Meta{}))
u, err := user_model.GetUserByEmail(db.DefaultContext, v.user.Email) u, err := user_model.GetUserByEmail(db.DefaultContext, v.user.Email)
assert.NoError(t, err) assert.NoError(t, err)