CVE-2026-11439
Description
Improper authorization in TheOpenDev allows unauthorized cross-project repository forking, potentially exposing private data.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Improper authorization in TheOpenDev allows unauthorized cross-project repository forking, potentially exposing private data.
Vulnerability
A vulnerability exists in TheOpenDev up to version 15.0.5 within the Parent Project Handler component, specifically concerning the /projects endpoint. The manipulation of the project.parentId argument, particularly the project.forkedFromId property, allows for improper authorization checks when creating a forked project. This affects the file /projects/.
Exploitation
An attacker with the ability to create projects within a namespace can exploit this by setting the project.forkedFromId to reference a source project they should not have read access to. The system does not adequately validate the caller's authorization to read the source project before proceeding with the fork operation. The attack can be performed remotely [1].
Impact
Successful exploitation allows an attacker to create a fork of a private repository, copying its contents and metadata into a project they control. While the creator automatically gets owner-level access to the new project, the primary impact is the unauthorized replication of private repository contents and related metadata, potentially leading to data leakage [1].
Mitigation
This issue is resolved in TheOpenDev version 15.0.6. It is recommended to upgrade to this version or later. The fix involves requiring explicit source-project visibility and/or code-read authorization before accepting forkedFromId, and adding defensive verification within the fork service implementation [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.