CVE-2023-24828 (published 2023-02-08T00:15Z)

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.

CVSS v3 base score: 8.8, HIGH (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-338
cwe_name: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG)
description: The product uses a Pseudo-Random Number Generator (PRNG) in a security context, but the PRNG's algorithm is not cryptographically strong.
extended_description: 
            When a non-cryptographic PRNG is used in a cryptographic context, it can expose the cryptography to certain types of attacks.
            Often a pseudo-random number generator (PRNG) is not designed for cryptography. Sometimes a mediocre source of randomness is sufficient or preferable for algorithms that use random numbers. Weak generators generally take less processing power and/or do not use the precious, finite, entropy sources on a system. While such PRNGs might have very useful features, these same features could be used to break the cryptography.
         
         

Fix information:
repo_url: https://github.com/theonedev/onedev
commit_details: repo_url: https://github.com/theonedev/onedev
author: Robin Shen
author_date: 2023-02-07 14:12:45+08:00
msg: Fix issue #1179 - OneDev should use crypto strong random string for access token and password reset
repository: repo_url: https://github.com/theonedev/onedev
repo_name: theonedev/onedev
description: Git Server with CI/CD, Kanban, and Packages. Seamless integration. Unparalleled experience.
repo_language: Java
owner: theonedev
file_changes: filename: HookUtils.java
change_type: ModificationType.MODIFY
diff: @@ -1,26 +1,24 @@
 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;
 	

programming_language: Java
filename: DataMigrator.java
change_type: ModificationType.MODIFY
diff: @@ -10,9 +10,9 @@ 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;
@@ -2146,7 +2146,7 @@ public class DataMigrator {
 			} 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);
@@ -2692,7 +2692,7 @@ public class DataMigrator {
 					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");
@@ -4831,5 +4831,18 @@ public class DataMigrator {
 			}
 		}
 	}
+
+	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);
+			}
+		}
+	}
 	
 }

programming_language: Java
filename: EmailAddress.java
change_type: ModificationType.MODIFY
diff: @@ -1,24 +1,16 @@
 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")})
@@ -35,7 +27,7 @@ public class EmailAddress extends AbstractEntity {
     private String value;
     
     @JsonIgnore
-    private String verificationCode = RandomStringUtils.randomAlphanumeric(16);
+    private String verificationCode = CryptoUtils.generateSecret();
     
     private boolean primary;
     

programming_language: Java
filename: User.java
change_type: ModificationType.MODIFY
diff: @@ -1,41 +1,7 @@
 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;
@@ -51,12 +17,28 @@ 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(
@@ -68,8 +50,6 @@ 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;
@@ -117,7 +97,7 @@ public class User extends AbstractEntity implements AuthenticationInfo {
 	
 	@Column(unique=true, nullable=false)
 	@JsonIgnore
-	private String accessToken = RandomStringUtils.randomAlphanumeric(ACCESS_TOKEN_LEN);
+	private String accessToken = CryptoUtils.generateSecret();
 	
 	@JsonIgnore
 	@Lob

programming_language: Java
filename: WebHook.java
change_type: ModificationType.MODIFY
diff: @@ -1,21 +1,19 @@
 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 {
 
@@ -70,7 +68,7 @@ public class WebHook implements Serializable {
 	
 	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

programming_language: Java
filename: CryptoUtils.java
change_type: ModificationType.MODIFY
diff: @@ -1,22 +1,24 @@
 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;
@@ -40,7 +42,16 @@ public class CryptoUtils {
 		} 
 		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();
 	}

programming_language: Java
filename: AccessTokenPanel.java
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,11 @@
 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;
@@ -8,12 +13,6 @@ 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 {
 
@@ -50,7 +49,7 @@ public abstract class AccessTokenPanel extends Panel {
 
 			@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());

programming_language: Java
filename: TwoFactorAuthenticationSetupPanel.html
change_type: ModificationType.MODIFY
diff: @@ -46,10 +46,7 @@
 			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">

programming_language: HTML
filename: TwoFactorAuthenticationSetupPanel.java
change_type: ModificationType.MODIFY
diff: @@ -1,12 +1,13 @@
 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;
@@ -26,12 +27,10 @@ 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> {
@@ -50,7 +49,7 @@ public abstract class TwoFactorAuthenticationSetupPanel extends GenericPanel<Use
 		
 		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);
 
@@ -119,12 +118,8 @@ public abstract class TwoFactorAuthenticationSetupPanel extends GenericPanel<Use
 					
 					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() {
 

programming_language: Java
filename: PasswordResetPage.java
change_type: ModificationType.MODIFY
diff: @@ -1,20 +1,6 @@
 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;
@@ -25,8 +11,20 @@ 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 {
@@ -83,7 +81,7 @@ public class PasswordResetPage extends SimplePage {
 					} 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);
 							

programming_language: Java
filename: incompatibilities.md
change_type: ModificationType.MODIFY
diff: @@ -1,3 +1,7 @@
+# 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, 

programming_language: Markdown