diff --git a/CHANGELOG b/CHANGELOG
index bb3fe90729ab0581c8f3f64fe3e6c893da8bc885..a05cf772aa1d2bf34b1e1a49cf69c278d8b8b611 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -60,6 +60,7 @@ v 8.7.0 (unreleased)
   - API: Do not leak group existence via return code (Robert Schilling)
   - ClosingIssueExtractor regex now also works with colons. e.g. "Fixes: #1234" !3591
   - Update number of Todos in the sidebar when it's marked as "Done". !3600
+  - Sanitize branch names created for confidential issues
   - API: Expose 'updated_at' for issue, snippet, and merge request notes (Robert Schilling)
   - API: User can leave a project through the API when not master or owner. !3613
   - Fix repository cache invalidation issue when project is recreated with an empty repo (Stan Hu)
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index c26cfeccf1dd3a1896e0c630fc7d31756d0cb2b4..f2ae572cfe13fbd302dad6862630329193497e0d 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -128,10 +128,7 @@ class Projects::IssuesController < Projects::ApplicationController
   end
 
   def related_branches
-    merge_requests = @issue.referenced_merge_requests(current_user)
-
-    @related_branches = @issue.related_branches -
-      merge_requests.map(&:source_branch)
+    @related_branches = @issue.related_branches(current_user)
 
     respond_to do |format|
       format.json do
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 3f188e047709a4b8b2183b50514fa34a46672c1c..2f773869603e6a36ecc16d2f76fad14a2a6aaa93 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -104,10 +104,16 @@ class Issue < ActiveRecord::Base
     end
   end
 
-  def related_branches
-    project.repository.branch_names.select do |branch|
+  # All branches containing the current issue's ID, except for
+  # those with a merge request open referencing the current issue.
+  def related_branches(current_user)
+    branches_with_iid = project.repository.branch_names.select do |branch|
       branch =~ /\A#{iid}-(?!\d+-stable)/i
     end
+
+    branches_with_merge_request = self.referenced_merge_requests(current_user).map(&:source_branch)
+
+    branches_with_iid - branches_with_merge_request
   end
 
   # Reset issue events cache
@@ -151,13 +157,17 @@ class Issue < ActiveRecord::Base
   end
 
   def to_branch_name
-    "#{iid}-#{title.parameterize}"
+    if self.confidential?
+      "#{iid}-confidential-issue"
+    else
+      "#{iid}-#{title.parameterize}"
+    end
   end
 
   def can_be_worked_on?(current_user)
     !self.closed? &&
       !self.project.forked? &&
-      self.related_branches.empty? &&
+      self.related_branches(current_user).empty? &&
       self.closed_by_merge_requests(current_user).empty?
   end
 end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index fac516f9568c8a06ee2f316a0d6ad39637d7ce20..060e6599104530840d2fa6bcb26683d50dbec441 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -191,18 +191,36 @@ describe Issue, models: true do
   end
 
   describe '#related_branches' do
-    it 'selects the right branches' do
+    let(:user) { build(:admin) }
+
+    before do
       allow(subject.project.repository).to receive(:branch_names).
-        and_return(['mpempe', "#{subject.iid}mepmep", subject.to_branch_name])
+                                            and_return(["mpempe", "#{subject.iid}mepmep", subject.to_branch_name, "#{subject.iid}-branch"])
+
+      # Without this stub, the `create(:merge_request)` above fails because it can't find
+      # the source branch. This seems like a reasonable compromise, in comparison with
+      # setting up a full repo here.
+      allow_any_instance_of(MergeRequest).to receive(:create_merge_request_diff)
+    end
+
+    it "selects the right branches when there are no referenced merge requests" do
+      expect(subject.related_branches(user)).to eq([subject.to_branch_name, "#{subject.iid}-branch"])
+    end
 
-      expect(subject.related_branches).to eq([subject.to_branch_name])
+    it "selects the right branches when there is a referenced merge request" do
+      merge_request = create(:merge_request, { description: "Closes ##{subject.iid}",
+                                               source_project: subject.project,
+                                               source_branch: "#{subject.iid}-branch" })
+      merge_request.create_cross_references!(user)
+      expect(subject.referenced_merge_requests).to_not be_empty
+      expect(subject.related_branches(user)).to eq([subject.to_branch_name])
     end
 
     it 'excludes stable branches from the related branches' do
       allow(subject.project.repository).to receive(:branch_names).
         and_return(["#{subject.iid}-0-stable"])
 
-      expect(subject.related_branches).to eq []
+      expect(subject.related_branches(user)).to eq []
     end
   end
 
@@ -217,11 +235,20 @@ describe Issue, models: true do
     let(:subject) { create :issue }
   end
 
-  describe '#to_branch_name' do
-    let(:issue) { create(:issue, title: 'a' * 30) }
+  describe "#to_branch_name" do
+    let(:issue) { create(:issue, title: 'testing-issue') }
 
     it 'starts with the issue iid' do
-      expect(issue.to_branch_name).to match /\A#{issue.iid}-a+\z/
+      expect(issue.to_branch_name).to match /\A#{issue.iid}-[A-Za-z\-]+\z/
+    end
+
+    it "contains the issue title if not confidential" do
+      expect(issue.to_branch_name).to match /testing-issue\z/
+    end
+
+    it "does not contain the issue title if confidential" do
+      issue = create(:issue, title: 'testing-issue', confidential: true)
+      expect(issue.to_branch_name).to match /confidential-issue\z/
     end
   end
 end