CVE-2023-24828: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG) (CWE-338)
value: Onedev is a self-hosted Git Server with CI/CD and Kanban. In versions prior to 7.9.12 the algorithm used to generate access token and password reset keys was not cryptographically secure. Existing normal users (or everyone if it allows self-registration) may exploit this to elevate privilege to obtain administrator permission. This issue is has been addressed in version 7.9.12. Users are advised to upgrade. There are no known workarounds for this vulnerability.
CWE description: The product uses a Pseudo-Random Number Generator (PRNG) in a security context, but the PRNG's algorithm is not cryptographically strong.
CVSS v3 base score: 8.8
Published: 2023-02-08T00:15Z
Repository: https://github.com/theonedev/onedev
Language: Java
Files changed: server-core/src/main/java/io/onedev/server/git/hook/HookUtils.java, server-core/src/main/java/io/onedev/server/migration/DataMigrator.java, server-core/src/main/java/io/onedev/server/model/EmailAddress.java, server-core/src/main/java/io/onedev/server/model/User.java, server-core/src/main/java/io/onedev/server/model/support/WebHook.java, server-core/src/main/java/io/onedev/server/util/CryptoUtils.java, server-core/src/main/java/io/onedev/server/web/component/user/accesstoken/AccessTokenPanel.java, server-core/src/main/java/io/onedev/server/web/component/user/twofactorauthentication/TwoFactorAuthenticationSetupPanel.html, server-core/src/main/java/io/onedev/server/web/component/user/twofactorauthentication/TwoFactorAuthenticationSetupPanel.java, server-core/src/main/java/io/onedev/server/web/page/simple/security/PasswordResetPage.java, server-product/system/incompatibilities/incompatibilities.md
Commit date: 2023-02-07 14:12:45 +0800
Commit message: Fix issue #1179 - OneDev should use crypto strong random string for access token and password reset

Diff: diff --git a/server-core/src/main/java/io/onedev/server/git/hook/HookUtils.java b/server-core/src/main/java/io/onedev/server/git/hook/HookUtils.java
index 2af4206bf9d..db6b2ff9989 100644
--- a/server-core/src/main/java/io/onedev/server/git/hook/HookUtils.java
+++ b/server-core/src/main/java/io/onedev/server/git/hook/HookUtils.java
@@ -1,28 +1,26 @@
 package io.onedev.server.git.hook;
 
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.charset.Charset;
-import java.util.HashMap;
-import java.util.Map;
-
-import org.apache.commons.io.IOUtils;
-import org.apache.commons.lang3.RandomStringUtils;
-
 import com.google.common.base.Preconditions;
-
 import io.onedev.commons.utils.FileUtils;
 import io.onedev.commons.utils.StringUtils;
 import io.onedev.server.OneDev;
 import io.onedev.server.ServerConfig;
 import io.onedev.server.entitymanager.SettingManager;
+import io.onedev.server.util.CryptoUtils;
+import org.apache.commons.io.IOUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
 
 public class HookUtils {
 
-	public static final String HOOK_TOKEN = RandomStringUtils.randomAlphanumeric(20); 
+	public static final String HOOK_TOKEN = CryptoUtils.generateSecret(); 
 	
 	private static final String gitReceiveHook;
 	
 	static {
         try (InputStream is = HookUtils.class.getClassLoader().getResourceAsStream("git-receive-hook")) {
diff --git a/server-core/src/main/java/io/onedev/server/migration/DataMigrator.java b/server-core/src/main/java/io/onedev/server/migration/DataMigrator.java
index f2458400894..affabbd1445 100644
--- a/server-core/src/main/java/io/onedev/server/migration/DataMigrator.java
+++ b/server-core/src/main/java/io/onedev/server/migration/DataMigrator.java
@@ -8,13 +8,13 @@ import io.onedev.commons.utils.FileUtils;
 import io.onedev.commons.utils.StringUtils;
 import io.onedev.server.OneDev;
 import io.onedev.server.markdown.MarkdownManager;
 import io.onedev.server.markdown.MentionParser;
 import io.onedev.server.model.*;
+import io.onedev.server.util.CryptoUtils;
 import io.onedev.server.util.Pair;
 import org.apache.commons.io.IOUtils;
-import org.apache.commons.lang3.RandomStringUtils;
 import org.apache.commons.lang3.tuple.Triple;
 import org.dom4j.Element;
 import org.dom4j.Node;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -2144,11 +2144,11 @@ public class DataMigrator {
 				}
 				dom.writeToFile(file, false);
 			} else if (file.getName().startsWith("Users.xml")) {
 				VersionedXmlDoc dom = VersionedXmlDoc.fromFile(file);
 				for (Element element: dom.getRootElement().elements()) {
-					element.addElement("accessToken").setText(RandomStringUtils.randomAlphanumeric(40));
+					element.addElement("accessToken").setText(CryptoUtils.generateSecret());
 					element.addElement("ssoInfo").addElement("subject").setText(UUID.randomUUID().toString());
 				}
 				dom.writeToFile(file, false);
 			} else if (file.getName().startsWith("PullRequestUpdates.xml")) {
 				VersionedXmlDoc dom = VersionedXmlDoc.fromFile(file);
@@ -2690,11 +2690,11 @@ public class DataMigrator {
 					element.addElement("name").setText("Unknown");
 					element.addElement("password").setText("no password");
 					element.addElement("ssoInfo").addElement("subject").setText(UUID.randomUUID().toString());
 					element.addElement("email").setText("unknown email");
 					element.addElement("alternateEmails");
-					element.addElement("accessToken").setText(RandomStringUtils.randomAlphanumeric(User.ACCESS_TOKEN_LEN));
+					element.addElement("accessToken").setText(CryptoUtils.generateSecret());
 					element.addElement("userProjectQueries");
 					element.addElement("userIssueQueries");
 					element.addElement("userIssueQueryWatches");
 					element.addElement("issueQueryWatches");
 					
@@ -4829,7 +4829,20 @@ public class DataMigrator {
 				}
 				dom.writeToFile(file, false);
 			}
 		}
 	}
+
+	private void migrate110(File dataDir, Stack<Integer> versions) {
+		var updateIds = new HashSet<>();
+		for (File file: dataDir.listFiles()) {
+			if (file.getName().startsWith("Users.xml")) {
+				VersionedXmlDoc dom = VersionedXmlDoc.fromFile(file);
+				for (Element element: dom.getRootElement().elements()) {
+					element.element("accessToken").setText(CryptoUtils.generateSecret());
+				}
+				dom.writeToFile(file, false);
+			}
+		}
+	}
 	
 }
diff --git a/server-core/src/main/java/io/onedev/server/model/EmailAddress.java b/server-core/src/main/java/io/onedev/server/model/EmailAddress.java
index 2bda8929d17..f5cf1ae92af 100644
--- a/server-core/src/main/java/io/onedev/server/model/EmailAddress.java
+++ b/server-core/src/main/java/io/onedev/server/model/EmailAddress.java
@@ -1,26 +1,18 @@
 package io.onedev.server.model;
 
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.FetchType;
-import javax.persistence.Index;
-import javax.persistence.JoinColumn;
-import javax.persistence.ManyToOne;
-import javax.persistence.Table;
-
-import org.apache.commons.lang3.RandomStringUtils;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.onedev.server.util.CryptoUtils;
+import io.onedev.server.util.facade.EmailAddressFacade;
+import io.onedev.server.web.editable.annotation.Editable;
 import org.hibernate.annotations.Cache;
 import org.hibernate.annotations.CacheConcurrencyStrategy;
+
+import javax.persistence.*;
 import javax.validation.constraints.Email;
 import javax.validation.constraints.NotEmpty;
 
-import com.fasterxml.jackson.annotation.JsonIgnore;
-
-import io.onedev.server.util.facade.EmailAddressFacade;
-import io.onedev.server.web.editable.annotation.Editable;
-
 @Editable
 @Entity
 @Table(indexes={@Index(columnList="o_owner_id"), @Index(columnList="value")})
 @Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
 public class EmailAddress extends AbstractEntity {
@@ -33,11 +25,11 @@ public class EmailAddress extends AbstractEntity {
     
     @Column(nullable=false, unique=true)
     private String value;
     
     @JsonIgnore
-    private String verificationCode = RandomStringUtils.randomAlphanumeric(16);
+    private String verificationCode = CryptoUtils.generateSecret();
     
     private boolean primary;
     
     private boolean git;
     
diff --git a/server-core/src/main/java/io/onedev/server/model/User.java b/server-core/src/main/java/io/onedev/server/model/User.java
index d187471e22d..cdad76b4460 100644
--- a/server-core/src/main/java/io/onedev/server/model/User.java
+++ b/server-core/src/main/java/io/onedev/server/model/User.java
@@ -1,43 +1,9 @@
 package io.onedev.server.model;
 
-import static io.onedev.server.model.User.PROP_ACCESS_TOKEN;
-import static io.onedev.server.model.User.PROP_FULL_NAME;
-import static io.onedev.server.model.User.PROP_NAME;
-import static io.onedev.server.model.User.PROP_SSO_CONNECTOR;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Optional;
-import java.util.Stack;
-import java.util.stream.Collectors;
-
-import javax.annotation.Nullable;
-import javax.persistence.CascadeType;
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.Index;
-import javax.persistence.Lob;
-import javax.persistence.OneToMany;
-import javax.persistence.Table;
-
-import org.apache.commons.lang3.RandomStringUtils;
-import org.apache.shiro.authc.AuthenticationInfo;
-import org.apache.shiro.subject.PrincipalCollection;
-import org.apache.shiro.subject.SimplePrincipalCollection;
-import org.apache.shiro.subject.Subject;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.hibernate.annotations.Cache;
-import org.hibernate.annotations.CacheConcurrencyStrategy;
-import javax.validation.constraints.NotEmpty;
-
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.google.common.base.MoreObjects;
-
 import edu.emory.mathcs.backport.java.util.Collections;
 import io.onedev.commons.utils.ExplicitException;
 import io.onedev.server.OneDev;
 import io.onedev.server.entitymanager.EmailAddressManager;
 import io.onedev.server.entitymanager.SettingManager;
@@ -49,16 +15,32 @@ import io.onedev.server.model.support.administration.authenticator.Authenticator
 import io.onedev.server.model.support.administration.sso.SsoConnector;
 import io.onedev.server.model.support.build.NamedBuildQuery;
 import io.onedev.server.model.support.issue.NamedIssueQuery;
 import io.onedev.server.model.support.pullrequest.NamedPullRequestQuery;
 import io.onedev.server.security.SecurityUtils;
+import io.onedev.server.util.CryptoUtils;
 import io.onedev.server.util.facade.UserFacade;
 import io.onedev.server.util.validation.annotation.UserName;
 import io.onedev.server.util.watch.QuerySubscriptionSupport;
 import io.onedev.server.util.watch.QueryWatchSupport;
 import io.onedev.server.web.editable.annotation.Editable;
 import io.onedev.server.web.editable.annotation.Password;
+import org.apache.shiro.authc.AuthenticationInfo;
+import org.apache.shiro.subject.PrincipalCollection;
+import org.apache.shiro.subject.SimplePrincipalCollection;
+import org.apache.shiro.subject.Subject;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.hibernate.annotations.Cache;
+import org.hibernate.annotations.CacheConcurrencyStrategy;
+
+import javax.annotation.Nullable;
+import javax.persistence.*;
+import javax.validation.constraints.NotEmpty;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static io.onedev.server.model.User.*;
 
 @Entity
 @Table(
 		indexes={@Index(columnList=PROP_NAME), @Index(columnList=PROP_FULL_NAME), 
 				@Index(columnList=PROP_SSO_CONNECTOR), @Index(columnList=PROP_ACCESS_TOKEN)})
@@ -66,12 +48,10 @@ import io.onedev.server.web.editable.annotation.Password;
 @Editable
 public class User extends AbstractEntity implements AuthenticationInfo {
 
 	private static final long serialVersionUID = 1L;
 	
-	public static final int ACCESS_TOKEN_LEN = 40;
-	
 	public static final Long UNKNOWN_ID = -2L;
 	
 	public static final Long SYSTEM_ID = -1L;
 	
 	public static final Long ROOT_ID = 1L;
@@ -115,11 +95,11 @@ public class User extends AbstractEntity implements AuthenticationInfo {
 	@JsonIgnore
 	private String ssoConnector;
 	
 	@Column(unique=true, nullable=false)
 	@JsonIgnore
-	private String accessToken = RandomStringUtils.randomAlphanumeric(ACCESS_TOKEN_LEN);
+	private String accessToken = CryptoUtils.generateSecret();
 	
 	@JsonIgnore
 	@Lob
 	@Column(length=65535)
 	private TwoFactorAuthentication twoFactorAuthentication;
diff --git a/server-core/src/main/java/io/onedev/server/model/support/WebHook.java b/server-core/src/main/java/io/onedev/server/model/support/WebHook.java
index 157ab972928..6df3000b065 100644
--- a/server-core/src/main/java/io/onedev/server/model/support/WebHook.java
+++ b/server-core/src/main/java/io/onedev/server/model/support/WebHook.java
@@ -1,23 +1,21 @@
 package io.onedev.server.model.support;
 
-import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.List;
-
-import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.Size;
-
-import org.apache.commons.lang3.RandomStringUtils;
-
 import io.onedev.server.event.project.RefUpdated;
 import io.onedev.server.event.project.build.BuildEvent;
 import io.onedev.server.event.project.codecomment.CodeCommentEvent;
 import io.onedev.server.event.project.issue.IssueEvent;
 import io.onedev.server.event.project.pullrequest.PullRequestEvent;
+import io.onedev.server.util.CryptoUtils;
 import io.onedev.server.web.editable.annotation.Editable;
 
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.Size;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
 @Editable
 public class WebHook implements Serializable {
 
 	private static final long serialVersionUID = 1L;
 
@@ -68,11 +66,11 @@ public class WebHook implements Serializable {
 	
 	private String postUrl;
 	
 	private List<EventType> eventTypes = new ArrayList<>();
 	
-	private String secret = RandomStringUtils.randomAlphanumeric(20);
+	private String secret = CryptoUtils.generateSecret();
 
 	@Editable(order=100, description="The URL of the server endpoint that will receive the webhook POST requests")
 	@NotEmpty
 	public String getPostUrl() {
 		return postUrl;
diff --git a/server-core/src/main/java/io/onedev/server/util/CryptoUtils.java b/server-core/src/main/java/io/onedev/server/util/CryptoUtils.java
index 4a9b9d23bd6..0a87898104c 100644
--- a/server-core/src/main/java/io/onedev/server/util/CryptoUtils.java
+++ b/server-core/src/main/java/io/onedev/server/util/CryptoUtils.java
@@ -1,24 +1,26 @@
 package io.onedev.server.util;
 
-import java.security.NoSuchAlgorithmException;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.KeySpec;
-
-import javax.crypto.SecretKeyFactory;
-import javax.crypto.spec.PBEKeySpec;
-
+import io.onedev.server.OneDev;
+import io.onedev.server.entitymanager.SettingManager;
 import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang3.RandomStringUtils;
 import org.apache.shiro.crypto.AesCipherService;
 import org.apache.sshd.common.digest.BaseDigest;
 import org.apache.sshd.common.digest.Digest;
 
-import io.onedev.server.OneDev;
-import io.onedev.server.entitymanager.SettingManager;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
 
 public class CryptoUtils {
-
+	
+	private static final int DEFAULT_SECRET_LEN = 40;
+	
     public static final Digest DIGEST_FORMAT = new BaseDigest("MD5", 512);
 	
 	private static volatile KeyPair keyPair;
 	
     private static AesCipherService cipherService = new AesCipherService();
@@ -38,11 +40,20 @@ public class CryptoUtils {
 				throw new RuntimeException(e);
 			}		
 		} 
 		return keyPair.getCipherKey();
 	}
+	
+	public static String generateSecret(int count) {
+		return RandomStringUtils.random(count, 0, 0, true, true, null, 
+				new SecureRandom());		
+	}
 
+	public static String generateSecret() {
+		return generateSecret(DEFAULT_SECRET_LEN);
+	}
+	
 	public static byte[] encrypt(byte[] data) {
 		return cipherService.encrypt(data, getCipherKey()).getBytes();
 	}
 	
 	public static byte[] decrypt(byte[] data) {
diff --git a/server-core/src/main/java/io/onedev/server/web/component/user/accesstoken/AccessTokenPanel.java b/server-core/src/main/java/io/onedev/server/web/component/user/accesstoken/AccessTokenPanel.java
index ad5f96d28a6..5d053b4b889 100644
--- a/server-core/src/main/java/io/onedev/server/web/component/user/accesstoken/AccessTokenPanel.java
+++ b/server-core/src/main/java/io/onedev/server/web/component/user/accesstoken/AccessTokenPanel.java
@@ -1,21 +1,20 @@
 package io.onedev.server.web.component.user.accesstoken;
 
-import org.apache.commons.lang3.RandomStringUtils;
+import io.onedev.server.OneDev;
+import io.onedev.server.entitymanager.UserManager;
+import io.onedev.server.model.User;
+import io.onedev.server.util.CryptoUtils;
+import io.onedev.server.web.component.link.copytoclipboard.CopyToClipboardLink;
+import io.onedev.server.web.util.ConfirmClickModifier;
 import org.apache.wicket.Session;
 import org.apache.wicket.markup.html.form.TextField;
 import org.apache.wicket.markup.html.link.Link;
 import org.apache.wicket.markup.html.panel.Panel;
 import org.apache.wicket.model.AbstractReadOnlyModel;
 import org.apache.wicket.model.IModel;
 
-import io.onedev.server.OneDev;
-import io.onedev.server.entitymanager.UserManager;
-import io.onedev.server.model.User;
-import io.onedev.server.web.component.link.copytoclipboard.CopyToClipboardLink;
-import io.onedev.server.web.util.ConfirmClickModifier;
-
 @SuppressWarnings("serial")
 public abstract class AccessTokenPanel extends Panel {
 
 	public AccessTokenPanel(String id) {
 		super(id);
@@ -48,11 +47,11 @@ public abstract class AccessTokenPanel extends Panel {
 		
 		add(new Link<Void>("regenerate") {
 
 			@Override
 			public void onClick() {
-				getUser().setAccessToken(RandomStringUtils.randomAlphanumeric(User.ACCESS_TOKEN_LEN));
+				getUser().setAccessToken(CryptoUtils.generateSecret());
 				OneDev.getInstance(UserManager.class).save(getUser());
 				Session.get().success("Access token regenerated");
 				setResponsePage(getPage());
 			}
 			
diff --git a/server-core/src/main/java/io/onedev/server/web/component/user/twofactorauthentication/TwoFactorAuthenticationSetupPanel.html b/server-core/src/main/java/io/onedev/server/web/component/user/twofactorauthentication/TwoFactorAuthenticationSetupPanel.html
index 1764376e801..a42c6e81ccf 100644
--- a/server-core/src/main/java/io/onedev/server/web/component/user/twofactorauthentication/TwoFactorAuthenticationSetupPanel.html
+++ b/server-core/src/main/java/io/onedev/server/web/component/user/twofactorauthentication/TwoFactorAuthenticationSetupPanel.html
@@ -44,14 +44,11 @@
 			recovery codes below and keep 
 			them secret. These codes can be used to provide one-time access to your account in case you 
 			can not access the authentication application. They will <b>NOT</b> be displayed again 
 			</div>
 			<div>
-				<div wicket:id="recoveryCodes" class="d-flex justify-content-between text-monospace font-size-lg font-weight-bold">
-					<div wicket:id="left"></div>
-					<div wicket:id="right"></div>
-				</div>		
+				<div wicket:id="recoveryCodes" class="d-flex justify-content-center text-monospace font-size-sm font-weight-bold"></div>		
 			</div>
 		</div>
 		<div class="modal-footer d-flex justify-content-center">
 			<a wicket:id="ok" class="btn btn-primary">Ok</a>
 		</div>
diff --git a/server-core/src/main/java/io/onedev/server/web/component/user/twofactorauthentication/TwoFactorAuthenticationSetupPanel.java b/server-core/src/main/java/io/onedev/server/web/component/user/twofactorauthentication/TwoFactorAuthenticationSetupPanel.java
index 846d0ffaba6..150abff0278 100644
--- a/server-core/src/main/java/io/onedev/server/web/component/user/twofactorauthentication/TwoFactorAuthenticationSetupPanel.java
+++ b/server-core/src/main/java/io/onedev/server/web/component/user/twofactorauthentication/TwoFactorAuthenticationSetupPanel.java
@@ -1,14 +1,15 @@
 package io.onedev.server.web.component.user.twofactorauthentication;
 
-import java.io.IOException;
-import java.security.SecureRandom;
-import java.util.ArrayList;
-import java.util.List;
-
+import io.onedev.commons.utils.StringUtils;
+import io.onedev.server.OneDev;
+import io.onedev.server.entitymanager.UserManager;
+import io.onedev.server.model.User;
+import io.onedev.server.model.support.TwoFactorAuthentication;
+import io.onedev.server.util.CryptoUtils;
+import io.onedev.server.web.page.simple.security.LoginPage;
 import org.apache.commons.codec.binary.Base32;
-import org.apache.commons.lang3.RandomStringUtils;
 import org.apache.tika.mime.MediaType;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.ajax.markup.html.AjaxLink;
 import org.apache.wicket.ajax.markup.html.form.AjaxButton;
 import org.apache.wicket.feedback.FencedFeedbackPanel;
@@ -24,16 +25,14 @@ import org.apache.wicket.markup.html.panel.GenericPanel;
 import org.apache.wicket.markup.repeater.RepeatingView;
 import org.apache.wicket.model.Model;
 import org.apache.wicket.request.resource.AbstractResource;
 import org.apache.wicket.request.resource.ContentDisposition;
 
-import io.onedev.commons.utils.StringUtils;
-import io.onedev.server.OneDev;
-import io.onedev.server.entitymanager.UserManager;
-import io.onedev.server.model.User;
-import io.onedev.server.model.support.TwoFactorAuthentication;
-import io.onedev.server.web.page.simple.security.LoginPage;
+import java.io.IOException;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.List;
 
 @SuppressWarnings("serial")
 public abstract class TwoFactorAuthenticationSetupPanel extends GenericPanel<User> {
 
 	private static final int QR_CODE_SIZE = 160;
@@ -48,11 +47,11 @@ public abstract class TwoFactorAuthenticationSetupPanel extends GenericPanel<Use
 
 		String secretKey = generateSecretKey();
 		
 		List<String> scratchCodes = new ArrayList<>();
 		for (int i=0; i<16; i++) 
-			scratchCodes.add(RandomStringUtils.randomAlphanumeric(12));
+			scratchCodes.add(CryptoUtils.generateSecret());
 		
 		TwoFactorAuthentication authentication = new TwoFactorAuthentication(secretKey, scratchCodes);
 
 		Fragment fragment = new Fragment("content", "pendingVerifyFrag", this);
 		Form<?> form = new Form<Void>("form");
@@ -117,16 +116,12 @@ public abstract class TwoFactorAuthenticationSetupPanel extends GenericPanel<Use
 					getUser().setTwoFactorAuthentication(authentication);
 					OneDev.getInstance(UserManager.class).save(getUser());
 					
 					Fragment fragment = new Fragment("content", "verifiedFrag", TwoFactorAuthenticationSetupPanel.this);
 					RepeatingView recoveryCodesView = new RepeatingView("recoveryCodes");
-					for (int i=0; i<authentication.getScratchCodes().size()/2; i++) {
-						WebMarkupContainer container = new WebMarkupContainer(recoveryCodesView.newChildId());
-						container.add(new Label("left", authentication.getScratchCodes().get(i*2)));
-						container.add(new Label("right", authentication.getScratchCodes().get(i*2+1)));
-						recoveryCodesView.add(container);
-					}
+					for (String scratchCode: authentication.getScratchCodes())
+						recoveryCodesView.add(new Label(recoveryCodesView.newChildId(), scratchCode));
 					fragment.add(recoveryCodesView);
 					fragment.add(new ResourceLink<Void>("download", new AbstractResource() {
 
 						@Override
 						protected ResourceResponse newResourceResponse(Attributes attributes) {
diff --git a/server-core/src/main/java/io/onedev/server/web/page/simple/security/PasswordResetPage.java b/server-core/src/main/java/io/onedev/server/web/page/simple/security/PasswordResetPage.java
index a35093c5474..ca4d16471a7 100644
--- a/server-core/src/main/java/io/onedev/server/web/page/simple/security/PasswordResetPage.java
+++ b/server-core/src/main/java/io/onedev/server/web/page/simple/security/PasswordResetPage.java
@@ -1,34 +1,32 @@
 package io.onedev.server.web.page.simple.security;
 
-import java.util.Arrays;
-
-import org.apache.commons.lang3.RandomStringUtils;
-import org.apache.shiro.authc.credential.PasswordService;
-import org.apache.wicket.ajax.AjaxRequestTarget;
-import org.apache.wicket.feedback.FencedFeedbackPanel;
-import org.apache.wicket.markup.html.form.Form;
-import org.apache.wicket.markup.html.form.TextField;
-import org.apache.wicket.markup.html.link.Link;
-import org.apache.wicket.model.IModel;
-import org.apache.wicket.model.Model;
-import org.apache.wicket.request.mapper.parameter.PageParameters;
-
 import com.google.common.collect.Lists;
-
 import io.onedev.commons.loader.AppLoader;
 import io.onedev.commons.utils.ExplicitException;
 import io.onedev.commons.utils.TaskLogger;
 import io.onedev.server.OneDev;
 import io.onedev.server.entitymanager.SettingManager;
 import io.onedev.server.entitymanager.UserManager;
 import io.onedev.server.mail.MailManager;
 import io.onedev.server.model.EmailAddress;
 import io.onedev.server.model.User;
 import io.onedev.server.persistence.SessionManager;
+import io.onedev.server.util.CryptoUtils;
 import io.onedev.server.web.component.taskbutton.TaskButton;
 import io.onedev.server.web.page.simple.SimplePage;
+import org.apache.shiro.authc.credential.PasswordService;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.feedback.FencedFeedbackPanel;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.form.TextField;
+import org.apache.wicket.markup.html.link.Link;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+
+import java.util.Arrays;
 
 @SuppressWarnings("serial")
 public class PasswordResetPage extends SimplePage {
 
 	private String loginNameOrEmail;
@@ -81,11 +79,11 @@ public class PasswordResetPage extends SimplePage {
 					if (user == null) {
 						throw new ExplicitException("No user found with login name or verified email: " + loginNameOrEmail);
 					} else {
 						SettingManager settingManager = OneDev.getInstance(SettingManager.class);
 						if (settingManager.getMailSetting() != null) {
-							String password = RandomStringUtils.random(10, true, true);								
+							String password = CryptoUtils.generateSecret();								
 							user.setPassword(AppLoader.getInstance(PasswordService.class).encryptPassword(password));
 							userManager.save(user);
 							
 							MailManager mailManager = OneDev.getInstance(MailManager.class);
 							
diff --git a/server-product/system/incompatibilities/incompatibilities.md b/server-product/system/incompatibilities/incompatibilities.md
index 0e36a82bf57..2e387e72a18 100644
--- a/server-product/system/incompatibilities/incompatibilities.md
+++ b/server-product/system/incompatibilities/incompatibilities.md
@@ -1,5 +1,9 @@
+# 7.9.12
+
+All access tokens of users are re-generated with cryto strong random string. As a result of this, all places using previous access token should be replaced with the new access token. This may include restful api authentication, CI/CD checkout step using access token etc.  
+
 # 7.8.0
 
 To use consistent project url for web and git access, OneDev now uses project path instead of project id to access projects via web UI, for instance `https://code.onedev.io/projects/160` has been changed to `https://code.onedev.io/onedev/server`. To avoid conflicting with this url scheme, 
 some service urls have been changed and this causes some incompatibitlities:
 

Vulnerable code: import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.RandomStringUtils;


	public static final String HOOK_TOKEN = RandomStringUtils.randomAlphanumeric(20); 
import org.apache.commons.lang3.RandomStringUtils;
					element.addElement("accessToken").setText(RandomStringUtils.randomAlphanumeric(40));
					element.addElement("accessToken").setText(RandomStringUtils.randomAlphanumeric(User.ACCESS_TOKEN_LEN));
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Index;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

import org.apache.commons.lang3.RandomStringUtils;
import com.fasterxml.jackson.annotation.JsonIgnore;

import io.onedev.server.util.facade.EmailAddressFacade;
import io.onedev.server.web.editable.annotation.Editable;

    private String verificationCode = RandomStringUtils.randomAlphanumeric(16);
import static io.onedev.server.model.User.PROP_ACCESS_TOKEN;
import static io.onedev.server.model.User.PROP_FULL_NAME;
import static io.onedev.server.model.User.PROP_NAME;
import static io.onedev.server.model.User.PROP_SSO_CONNECTOR;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Stack;
import java.util.stream.Collectors;

import javax.annotation.Nullable;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Index;
import javax.persistence.Lob;
import javax.persistence.OneToMany;
import javax.persistence.Table;

import org.apache.commons.lang3.RandomStringUtils;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.eclipse.jgit.lib.PersonIdent;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import javax.validation.constraints.NotEmpty;


	public static final int ACCESS_TOKEN_LEN = 40;
	
	private String accessToken = RandomStringUtils.randomAlphanumeric(ACCESS_TOKEN_LEN);
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;

import org.apache.commons.lang3.RandomStringUtils;

	private String secret = RandomStringUtils.randomAlphanumeric(20);
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

import io.onedev.server.OneDev;
import io.onedev.server.entitymanager.SettingManager;

import org.apache.commons.lang3.RandomStringUtils;
import io.onedev.server.OneDev;
import io.onedev.server.entitymanager.UserManager;
import io.onedev.server.model.User;
import io.onedev.server.web.component.link.copytoclipboard.CopyToClipboardLink;
import io.onedev.server.web.util.ConfirmClickModifier;

				getUser().setAccessToken(RandomStringUtils.randomAlphanumeric(User.ACCESS_TOKEN_LEN));
				<div wicket:id="recoveryCodes" class="d-flex justify-content-between text-monospace font-size-lg font-weight-bold">
					<div wicket:id="left"></div>
					<div wicket:id="right"></div>
				</div>		
import java.io.IOException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.lang3.RandomStringUtils;
import io.onedev.commons.utils.StringUtils;
import io.onedev.server.OneDev;
import io.onedev.server.entitymanager.UserManager;
import io.onedev.server.model.User;
import io.onedev.server.model.support.TwoFactorAuthentication;
import io.onedev.server.web.page.simple.security.LoginPage;
			scratchCodes.add(RandomStringUtils.randomAlphanumeric(12));
					for (int i=0; i<authentication.getScratchCodes().size()/2; i++) {
						WebMarkupContainer container = new WebMarkupContainer(recoveryCodesView.newChildId());
						container.add(new Label("left", authentication.getScratchCodes().get(i*2)));
						container.add(new Label("right", authentication.getScratchCodes().get(i*2+1)));
						recoveryCodesView.add(container);
					}
import java.util.Arrays;

import org.apache.commons.lang3.RandomStringUtils;
import org.apache.shiro.authc.credential.PasswordService;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.feedback.FencedFeedbackPanel;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.request.mapper.parameter.PageParameters;


							String password = RandomStringUtils.random(10, true, true);								
Fixed code: import io.onedev.server.util.CryptoUtils;
import org.apache.commons.io.IOUtils;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
	public static final String HOOK_TOKEN = CryptoUtils.generateSecret(); 
import io.onedev.server.util.CryptoUtils;
					element.addElement("accessToken").setText(CryptoUtils.generateSecret());
					element.addElement("accessToken").setText(CryptoUtils.generateSecret());

	private void migrate110(File dataDir, Stack<Integer> versions) {
		var updateIds = new HashSet<>();
		for (File file: dataDir.listFiles()) {
			if (file.getName().startsWith("Users.xml")) {
				VersionedXmlDoc dom = VersionedXmlDoc.fromFile(file);
				for (Element element: dom.getRootElement().elements()) {
					element.element("accessToken").setText(CryptoUtils.generateSecret());
				}
				dom.writeToFile(file, false);
			}
		}
	}
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.onedev.server.util.CryptoUtils;
import io.onedev.server.util.facade.EmailAddressFacade;
import io.onedev.server.web.editable.annotation.Editable;

import javax.persistence.*;
    private String verificationCode = CryptoUtils.generateSecret();
import io.onedev.server.util.CryptoUtils;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.eclipse.jgit.lib.PersonIdent;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

import javax.annotation.Nullable;
import javax.persistence.*;
import javax.validation.constraints.NotEmpty;
import java.util.*;
import java.util.stream.Collectors;

import static io.onedev.server.model.User.*;
	private String accessToken = CryptoUtils.generateSecret();
import io.onedev.server.util.CryptoUtils;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

	private String secret = CryptoUtils.generateSecret();
import io.onedev.server.OneDev;
import io.onedev.server.entitymanager.SettingManager;
import org.apache.commons.lang3.RandomStringUtils;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
	
	private static final int DEFAULT_SECRET_LEN = 40;
	
	
	public static String generateSecret(int count) {
		return RandomStringUtils.random(count, 0, 0, true, true, null, 
				new SecureRandom());		
	}
	public static String generateSecret() {
		return generateSecret(DEFAULT_SECRET_LEN);
	}
	
import io.onedev.server.OneDev;
import io.onedev.server.entitymanager.UserManager;
import io.onedev.server.model.User;
import io.onedev.server.util.CryptoUtils;
import io.onedev.server.web.component.link.copytoclipboard.CopyToClipboardLink;
import io.onedev.server.web.util.ConfirmClickModifier;
				getUser().setAccessToken(CryptoUtils.generateSecret());
				<div wicket:id="recoveryCodes" class="d-flex justify-content-center text-monospace font-size-sm font-weight-bold"></div>		
import io.onedev.commons.utils.StringUtils;
import io.onedev.server.OneDev;
import io.onedev.server.entitymanager.UserManager;
import io.onedev.server.model.User;
import io.onedev.server.model.support.TwoFactorAuthentication;
import io.onedev.server.util.CryptoUtils;
import io.onedev.server.web.page.simple.security.LoginPage;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
			scratchCodes.add(CryptoUtils.generateSecret());
					for (String scratchCode: authentication.getScratchCodes())
						recoveryCodesView.add(new Label(recoveryCodesView.newChildId(), scratchCode));
import io.onedev.server.util.CryptoUtils;
import org.apache.shiro.authc.credential.PasswordService;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.feedback.FencedFeedbackPanel;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.request.mapper.parameter.PageParameters;

import java.util.Arrays;
							String password = CryptoUtils.generateSecret();								
# 7.9.12

All access tokens of users are re-generated with cryto strong random string. As a result of this, all places using previous access token should be replaced with the new access token. This may include restful api authentication, CI/CD checkout step using access token etc.