CVE-2026-11440
Description
Improper authorization in TheOpenDev's REST API allows unauthorized cross-project repository access via default branch manipulation.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Improper authorization in TheOpenDev's REST API allows unauthorized cross-project repository access via default branch manipulation.
Vulnerability
A vulnerability exists in TheOpenDev up to version 15.0.5 within the REST API's /repositories/{projectId}/default-branch endpoint. This issue stems from improper authorization when manipulating the project.defaultBranch argument, allowing unauthorized access to repository data. The affected component is the default branch handling within the API [1].
Exploitation
An attacker with the ability to create projects within a namespace can exploit this by setting the project.forkedFromId property during project creation to reference a private source project they do not have read access to. The system fails to validate the caller's read authorization for the source project before mirroring its contents and metadata into the attacker-controlled new project [1].
Impact
Successful exploitation allows an attacker to replicate the contents and metadata of a private repository into a project they fully control. Since newly created projects grant Owner-level authorization to the creator, the attacker can then grant themselves repository read access and retrieve the copied data through normal repository APIs, leading to unauthorized cross-project replication of sensitive information [1].
Mitigation
This vulnerability is mitigated by upgrading to TheOpenDev version 15.0.6 or later. The vendor recommends upgrading the affected component to address this security issue [2].
AI Insight generated on Jun 6, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
1bd7e41a55d76fix: Various security vulnerabilities in restful api (OD-2762)
4 files changed · +42 −20
server-core/src/main/java/io/onedev/server/ai/IssueHelper.java+5 −0 modified@@ -37,6 +37,11 @@ public static Map<String, Object> getSummary(Project currentProject, Issue issue summary.remove("pinDate"); summary.remove("boardPosition"); summary.remove("numberScopeId"); + summary.remove("totalEstimatedTime"); + summary.remove("totalSpentTime"); + summary.remove("ownEstimatedTime"); + summary.remove("ownSpentTime"); + summary.remove("progress"); summary.put("reference", issue.getReference().toString(currentProject)); summary.remove("submitterId"); summary.put("submitter", issue.getSubmitter().getName());
server-core/src/main/java/io/onedev/server/rest/resource/IssueResource.java+20 −6 modified@@ -31,6 +31,7 @@ import javax.ws.rs.core.Response; import org.apache.shiro.authz.UnauthorizedException; +import org.apache.shiro.subject.Subject; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -120,13 +121,27 @@ public IssueResource(SettingService settingService, IssueService issueService, @Api(order=100) @Path("/{issueId}") @GET - public Issue getIssue(@PathParam("issueId") Long issueId) { + public Map<String, Object> getIssue(@PathParam("issueId") Long issueId) { Issue issue = issueService.load(issueId); - if (!SecurityUtils.canAccessIssue(issue)) + var subject = SecurityUtils.getSubject(); + if (!SecurityUtils.canAccessIssue(subject, issue)) throw new UnauthorizedException(); - return issue; + return getIssueMap(subject, issue); } + private Map<String, Object> getIssueMap(Subject subject, Issue issue) { + var typeReference = new TypeReference<Map<String, Object>>() {}; + var issueMap = objectMapper.convertValue(issue, typeReference); + if (!SecurityUtils.canAccessTimeTracking(issue.getProject())) { + issueMap.remove("totalEstimatedTime"); + issueMap.remove("totalSpentTime"); + issueMap.remove("ownEstimatedTime"); + issueMap.remove("ownSpentTime"); + issueMap.remove("progress"); + } + return issueMap; + } + @Api(order=200, exampleProvider = "getFieldsExample") @Path("/{issueId}/fields") @GET @@ -176,7 +191,7 @@ public Collection<IssueComment> getComments(@PathParam("issueId") Long issueId) @GET public Collection<IssueWork> getWorks(@PathParam("issueId") Long issueId) { Issue issue = issueService.load(issueId); - if (!SecurityUtils.canAccessIssue(issue)) + if (!SecurityUtils.canAccessIssue(issue) || !SecurityUtils.canAccessTimeTracking(issue.getProject())) throw new UnauthorizedException(); return issue.getWorks(); } @@ -269,10 +284,9 @@ public List<Map<String, Object>> queryIssues( throw new NotAcceptableException("Error parsing query", e); } - var typeReference = new TypeReference<Map<String, Object>>() {}; var issues = new ArrayList<Map<String, Object>>(); for (var issue: issueService.query(subject, null, parsedQuery, false, offset, count)) { - var issueMap = objectMapper.convertValue(issue, typeReference); + var issueMap = getIssueMap(subject, issue); if (withFields != null && withFields) issueMap.put("fields", issue.getFields()); issues.add(issueMap);
server-core/src/main/java/io/onedev/server/rest/resource/ProjectResource.java+16 −13 modified@@ -296,12 +296,14 @@ public Long createProject(@NotNull @Valid ProjectData data) { checkProjectCreationPermission(subject, project.getParent()); if (project.getParent() != null && project.isSelfOrAncestorOf(project.getParent())) - throw new ExplicitException("Can not use current or descendant project as parent"); + throw new NotAcceptableException("Can not use current or descendant project as parent"); checkProjectNameDuplication(project); if (project.getForkedFrom() != null) { var forkedFrom = project.getForkedFrom(); + if (!SecurityUtils.canReadCode(subject, forkedFrom)) + throw new UnauthorizedException("Not authorized to read code of project '" + forkedFrom.getPath() + "'"); project.getBuildSetting().setBuildPreservations(forkedFrom.getBuildSetting().getBuildPreservations()); project.getBuildSetting().setCachePreserveDays(forkedFrom.getBuildSetting().getCachePreserveDays()); project.getBuildSetting().setJobProperties(forkedFrom.getBuildSetting().getJobProperties()); @@ -329,29 +331,30 @@ public Long createProject(@NotNull @Valid ProjectData data) { @Path("/{projectId}") @POST public Response updateProject(@PathParam("projectId") Long projectId, @NotNull @Valid ProjectData data) { - Project project = projectService.load(projectId); + Project project = projectService.load(projectId); + + var subject = SecurityUtils.getSubject(); + if (!SecurityUtils.canManageProject(subject, project)) + throw new UnauthorizedException(); + + Long oldParentId = Project.idOf(project.getParent()); var oldAuditContent = VersionedXmlDoc.fromBean(ProjectData.from(project)).toXML(); + data.populate(project, projectService); - + Project parent = data.getParentId() != null? projectService.load(data.getParentId()) : null; - Long oldParentId = Project.idOf(project.getParent()); - var subject = SecurityUtils.getSubject(); - if (!Objects.equals(oldParentId, Project.idOf(parent))) + if (!Objects.equals(oldParentId, Project.idOf(parent))) checkProjectCreationPermission(subject, parent); if (parent != null && project.isSelfOrAncestorOf(parent)) throw new ExplicitException("Can not use current or descendant project as parent"); checkProjectNameDuplication(project); - if (!SecurityUtils.canManageProject(subject, project)) { - throw new UnauthorizedException(); - } else { - projectService.update(project); - auditService.audit(project, "changed project via RESTful API", oldAuditContent, - VersionedXmlDoc.fromBean(ProjectData.from(project)).toXML()); - } + projectService.update(project); + auditService.audit(project, "changed project via RESTful API", oldAuditContent, + VersionedXmlDoc.fromBean(ProjectData.from(project)).toXML()); return Response.ok().build(); }
server-core/src/main/java/io/onedev/server/rest/resource/RepositoryResource.java+1 −1 modified@@ -115,7 +115,7 @@ public String getDefaultBranch(@PathParam("projectId") Long projectId) { @POST public Response setDefaultBranch(@PathParam("projectId") Long projectId, @NotNull String defaultBranch) { Project project = projectService.load(projectId); - if (!SecurityUtils.canWriteCode(project)) + if (!SecurityUtils.canManageProject(project)) throw new UnauthorizedException(); try {
Vulnerability mechanics
No source-code context for this CVE — mechanics is only generated when we can read the actual fix diff. Without that, the four sections (root cause, attack vector, affected code, fix) would be speculation rather than analysis.
References
6News mentions
0No linked articles in our index yet.