CVE-2023-24828 (Java, published 2023-02-08T00:15Z)
Description: 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.
Severity: nan (CVSS v3 base score: 8.8)
Weakness: CWE-338 \u2014 Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG)
The product uses a Pseudo-Random Number Generator (PRNG) in a security context, but the PRNG's algorithm is not cryptographically strong.
Repository: https://github.com/theonedev/onedev
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 message: Fix issue #1179 - OneDev should use crypto strong random string for access token and password reset


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.