CVE-2021-21246: Missing Authorization (CWE-862)
value: OneDev is an all-in-one devops platform. In OneDev before version 4.0.3, the REST UserResource endpoint performs a security check to make sure that only administrators can list user details. However for the `/users/{id}` endpoint there are no security checks enforced so it is possible to retrieve arbitrary user details including their Access Tokens! These access tokens can be used to access the API or clone code in the build spec via the HTTP(S) protocol. It has permissions to all projects accessible by the user account. This issue may lead to `Sensitive data leak` and leak the Access Token which can be used to impersonate the administrator or any other users. This issue was addressed in 4.0.3 by removing user info from restful api.
CWE description: The product does not perform an authorization check when an actor attempts to access a resource or perform an action.
CVSS v3 base score: 7.5
Published: 2021-01-15T21:15Z
Repository: https://github.com/theonedev/onedev
Language: Java
Files changed: server-core/src/main/java/io/onedev/server/model/User.java
Commit date: 2020-11-20 10:23:30 +0800
Commit message: Fix the issue that user access token can be leaked via restful api
Diff: 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 06a0656f9e..0edf231517 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
@@ -110,17 +110,19 @@ public class User extends AbstractEntity implements AuthenticationInfo {
 	@JsonView(DefaultView.class)
     private String password;
 
 	private String fullName;
 	
+	@JsonView(DefaultView.class)
 	@Embedded
 	private SsoInfo ssoInfo = new SsoInfo();
 	
 	@Column(unique=true, nullable=false)
 	private String email;
 	
 	@Column(unique=true, nullable=false)
+	@JsonView(DefaultView.class)
 	private String accessToken = RandomStringUtils.randomAlphanumeric(ACCESS_TOKEN_LEN);
 	
 	@OneToMany(mappedBy="user", cascade=CascadeType.REMOVE)
 	@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
 	private Collection<UserAuthorization> authorizations = new ArrayList<>();
diff --git a/server-core/src/main/java/io/onedev/server/rest/UserResource.java b/server-core/src/main/java/io/onedev/server/rest/UserResource.java
deleted file mode 100644
index 6c1d004e34..0000000000
--- a/server-core/src/main/java/io/onedev/server/rest/UserResource.java
+++ /dev/null
@@ -1,71 +0,0 @@
-package io.onedev.server.rest;
-
-import java.util.Collection;
-
-import javax.inject.Inject;
-import javax.inject.Singleton;
-import javax.ws.rs.Consumes;
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriInfo;
-
-import org.apache.shiro.authz.UnauthorizedException;
-import org.hibernate.criterion.Restrictions;
-import org.hibernate.validator.constraints.Email;
-
-import io.onedev.server.entitymanager.UserManager;
-import io.onedev.server.model.User;
-import io.onedev.server.persistence.dao.EntityCriteria;
-import io.onedev.server.rest.jersey.ValidQueryParams;
-import io.onedev.server.security.SecurityUtils;
-
-@Path("/users")
-@Consumes(MediaType.WILDCARD)
-@Produces(MediaType.APPLICATION_JSON)
-@Singleton
-public class UserResource {
-
-	private final UserManager userManager;
-	
-	@Inject
-	public UserResource(UserManager userManager) {
-		this.userManager = userManager;
-	}
-	
-	@ValidQueryParams
-	@GET
-	public Response query(@QueryParam("name") String name, @Email @QueryParam("email") String email, 
-			@QueryParam("offset") Integer offset, @QueryParam("count") Integer count, @Context UriInfo uriInfo) {
-    	if (!SecurityUtils.isAdministrator())
-    		throw new UnauthorizedException("Unauthorized access to user profiles");
-    	
-    	EntityCriteria<User> criteria = EntityCriteria.of(User.class);
-    	if (name != null)
-    		criteria.add(Restrictions.eq("name", name));
-		if (email != null)
-			criteria.add(Restrictions.eq("email", email));
-		
-    	if (offset == null)
-    		offset = 0;
-    	
-    	if (count == null || count > RestConstants.PAGE_SIZE) 
-    		count = RestConstants.PAGE_SIZE;
-
-    	Collection<User> users = userManager.query(criteria, offset, count);
-		
-		return Response.ok(users, RestConstants.JSON_UTF8).build();
-	}
-	
-    @GET
-    @Path("/{userId}")
-    public User get(@PathParam("userId") Long userId) {
-    	return userManager.load(userId);
-    }
-    
-}

Vulnerable code: package io.onedev.server.rest;

import java.util.Collection;

import javax.inject.Inject;
import javax.inject.Singleton;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;

import org.apache.shiro.authz.UnauthorizedException;
import org.hibernate.criterion.Restrictions;
import org.hibernate.validator.constraints.Email;

import io.onedev.server.entitymanager.UserManager;
import io.onedev.server.model.User;
import io.onedev.server.persistence.dao.EntityCriteria;
import io.onedev.server.rest.jersey.ValidQueryParams;
import io.onedev.server.security.SecurityUtils;

@Path("/users")
@Consumes(MediaType.WILDCARD)
@Produces(MediaType.APPLICATION_JSON)
@Singleton
public class UserResource {

	private final UserManager userManager;
	
	@Inject
	public UserResource(UserManager userManager) {
		this.userManager = userManager;
	}
	
	@ValidQueryParams
	@GET
	public Response query(@QueryParam("name") String name, @Email @QueryParam("email") String email, 
			@QueryParam("offset") Integer offset, @QueryParam("count") Integer count, @Context UriInfo uriInfo) {
    	if (!SecurityUtils.isAdministrator())
    		throw new UnauthorizedException("Unauthorized access to user profiles");
    	
    	EntityCriteria<User> criteria = EntityCriteria.of(User.class);
    	if (name != null)
    		criteria.add(Restrictions.eq("name", name));
		if (email != null)
			criteria.add(Restrictions.eq("email", email));
		
    	if (offset == null)
    		offset = 0;
    	
    	if (count == null || count > RestConstants.PAGE_SIZE) 
    		count = RestConstants.PAGE_SIZE;

    	Collection<User> users = userManager.query(criteria, offset, count);
		
		return Response.ok(users, RestConstants.JSON_UTF8).build();
	}
	
    @GET
    @Path("/{userId}")
    public User get(@PathParam("userId") Long userId) {
    	return userManager.load(userId);
    }
    
}
Fixed code: 	@JsonView(DefaultView.class)
	@JsonView(DefaultView.class)