Skip to content

Commit 5f3975d

Browse files
committed
Fix code injection bugs (XSS) in HTML views
- Replace unnecessary uses of innerHTML with textContent. - Prefer using the DOM over text interpolation. - Use the appropriate library and browser functions to escape HTML. - Rename variables to distinguish between HTML and plain text. - Don't directly send HTML templates to the git CLI, safely escape and interpolate the values after parsing CLI output.
1 parent 1c430c9 commit 5f3975d

File tree

12 files changed

+180
-102
lines changed

12 files changed

+180
-102
lines changed

Classes/Views/GLFileView.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
- (void)showFile;
2626
- (void)didLoad;
2727
- (NSString *)parseBlame:(NSString *)txt;
28-
- (NSString *)parseHTML:(NSString *)txt;
28+
- (NSString *)escapeHTML:(NSString *)txt;
2929

3030
@property NSMutableArray *groups;
3131
@property NSString *logFormat;

Classes/Views/GLFileView.m

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,6 @@ @implementation GLFileView
3636

3737
- (void) awakeFromNib
3838
{
39-
NSString *formatFile = [[NSBundle mainBundle] pathForResource:@"format" ofType:@"html" inDirectory:@"html/views/log"];
40-
if(formatFile!=nil)
41-
logFormat=[NSString stringWithContentsOfURL:[NSURL fileURLWithPath:formatFile] encoding:NSUTF8StringEncoding error:nil];
42-
43-
4439
startFile = GROUP_ID_FILEVIEW;
4540
//repository = historyController.repository;
4641
[super awakeFromNib];
@@ -87,11 +82,11 @@ - (void) showFile
8782

8883
NSString *fileTxt = @"";
8984
if([startFile isEqualToString:GROUP_ID_FILEVIEW])
90-
fileTxt = [self parseHTML:[file textContents]];
85+
fileTxt = [self escapeHTML:[file textContents]];
9186
else if([startFile isEqualToString:GROUP_ID_BLAME])
9287
fileTxt = [self parseBlame:[file blame]];
9388
else if([startFile isEqualToString:GROUP_ID_LOG])
94-
fileTxt = [file log:logFormat];
89+
fileTxt = [self htmlHistory:file];
9590

9691
id script = self.view.windowScriptObject;
9792
NSString *filePath = [file fullPath];
@@ -179,18 +174,15 @@ - (void)closeView
179174
[super closeView];
180175
}
181176

182-
- (NSString *) parseHTML:(NSString *)txt
177+
- (NSString *) escapeHTML:(NSString *)txt
183178
{
184-
txt=[txt stringByReplacingOccurrencesOfString:@"&" withString:@"&"];
185-
txt=[txt stringByReplacingOccurrencesOfString:@"<" withString:@"&lt;"];
186-
txt=[txt stringByReplacingOccurrencesOfString:@">" withString:@"&gt;"];
187-
188-
return txt;
179+
CFStringRef escaped = CFXMLCreateStringByEscapingEntities(NULL, (__bridge CFStringRef)txt, NULL);
180+
return (__bridge_transfer NSString *)escaped;
189181
}
190182

191183
- (NSString *) parseBlame:(NSString *)txt
192184
{
193-
txt=[self parseHTML:txt];
185+
txt=[self escapeHTML:txt];
194186

195187
NSArray *lines = [txt componentsSeparatedByString:@"\n"];
196188
NSString *line;
@@ -230,7 +222,7 @@ - (NSString *) parseBlame:(NSString *)txt
230222
if([summary length]>30){
231223
truncate_s=[summary substringWithRange:trunc];
232224
}
233-
NSString *block=[NSString stringWithFormat:@"<td><p class='author'><a href='' onclick='selectCommit(\"%@\"); return false;'>%@</a> %@</p><p class='summary'>%@</p></td>\n<td>\n",commitID,truncate_c,truncate_a,truncate_s];
225+
NSString *block=[NSString stringWithFormat:@"<td><p class='author'><a class='commit-link' href='#' data-commit-id='%@'>%@</a> %@</p><p class='summary'>%@</p></td>\n<td>\n",commitID,truncate_c,truncate_a,truncate_s];
234226
[headers setObject:block forKey:[header objectAtIndex:0]];
235227
}
236228
[res appendString:[headers objectForKey:[header objectAtIndex:0]]];
@@ -268,6 +260,38 @@ - (NSString *) parseBlame:(NSString *)txt
268260
return (NSString *)res;
269261
}
270262

263+
- (NSString *) htmlHistory:(PBGitTree *)file
264+
{
265+
// \0 can't be passed as a shell argument, so use a sufficiently long random seperator instead
266+
NSString *seperator = [[NSUUID UUID] UUIDString];
267+
NSString *commitTerminator = [[NSUUID UUID] UUIDString];
268+
NSString *logFormat = [[@"%h,%s,%aN,%ar,%H" stringByReplacingOccurrencesOfString:@"," withString:seperator] stringByAppendingString:commitTerminator];
269+
NSString *output = [file log:logFormat];
270+
NSArray<NSString *> *rawCommits = [output componentsSeparatedByString:commitTerminator];
271+
rawCommits = [rawCommits subarrayWithRange:(NSRange){0, rawCommits.count - 1}];
272+
273+
NSCharacterSet *whitespaceSet = [NSCharacterSet whitespaceCharacterSet];
274+
275+
NSMutableString *html = [NSMutableString string];
276+
for (NSString *rawCommit in rawCommits) {
277+
NSArray<NSString *> *parts = [rawCommit componentsSeparatedByString:seperator];
278+
[html appendFormat:
279+
@"<div id='%@' class='commit'>"
280+
"<p class='title'>%@</p>"
281+
"<table>"
282+
"<tr><td>Author:</td><td>%@</td></tr>"
283+
"<tr><td>Date:</td><td>%@</td></tr>"
284+
"<tr><td>Commit:</td><td><a class='commit-link' href='#'>%@</a></td></tr>"
285+
"</table>"
286+
"</div>",
287+
[self escapeHTML:[parts[0] stringByTrimmingCharactersInSet:whitespaceSet]], // trim leading newline from split
288+
[self escapeHTML:parts[1]],
289+
[self escapeHTML:parts[2]],
290+
[self escapeHTML:parts[3]],
291+
[self escapeHTML:parts[4]]];
292+
}
293+
return html;
294+
}
271295

272296

273297
#pragma mark NSSplitView delegate methods
@@ -334,6 +358,5 @@ - (void)restoreSplitViewPositiion
334358

335359

336360
@synthesize groups;
337-
@synthesize logFormat;
338361

339362
@end

Resources/html/lib/GitX.js

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ function $(element) {
99
return document.getElementById(element);
1010
}
1111

12-
String.prototype.escapeHTML = function() {
13-
return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
14-
};
15-
16-
String.prototype.unEscapeHTML = function() {
17-
return this.replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');
18-
};
12+
String.prototype.escapeHTML = (function() {
13+
var div = document.createElement("div");
14+
return function() {
15+
div.textContent = this;
16+
return div.innerHTML;
17+
};
18+
})();
1919

2020
Element.prototype.toggleDisplay = function() {
2121
if (this.style.display != "")
@@ -33,10 +33,10 @@ Array.prototype.indexOf = function(item, i) {
3333
return -1;
3434
};
3535

36-
var notify = function(text, state) {
36+
var notify = function(html, state) {
3737
var n = $("notification");
3838
n.style.display = "";
39-
$("notification_message").innerHTML = text;
39+
$("notification_message").innerHTML = html;
4040

4141
// Change color
4242
if (!state) { // Busy
@@ -55,3 +55,13 @@ var notify = function(text, state) {
5555
var hideNotification = function() {
5656
$("notification").style.display = "none";
5757
}
58+
59+
var bindCommitSelectionLinks = function(el) {
60+
var links = el.getElementsByClassName("commit-link");
61+
for (var i = 0, n = links.length; i < n; ++i) {
62+
links[i].addEventListener("click", function(e) {
63+
e.preventDefault();
64+
selectCommit(this.dataset.commitId || this.innerHTML);
65+
}, false);
66+
}
67+
};

Resources/html/lib/diffHighlighter.js

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,12 @@ var highlightDiff = function(diff, element, callbacks) {
8686

8787
// Show file list header
8888
finalContent += '<div class="file" id="file_index_' + (file_index - 1) + '">';
89-
finalContent += '<div id="title_' + title + '" class="expanded fileHeader"><a href="javascript:toggleDiff(\'' + title + '\');">' + title + '</a></div>';
89+
finalContent += '<div id="title_' + title + '" class="expanded fileHeader"><a href="">' + title + '</a></div>';
9090

9191
if (binary) {
9292
// diffContent is assumed to be empty for binary files
93-
if (callbacks["binaryFile"]) {
94-
finalContent += callbacks["binaryFile"](binaryname);
93+
if (callbacks.binaryFileHTML) {
94+
finalContent += callbacks.binaryFileHTML(binaryname);
9595
} else {
9696
finalContent += '<div id="content_' + title + '">Binary file differs</div>';
9797
}
@@ -254,6 +254,20 @@ var highlightDiff = function(diff, element, callbacks) {
254254
// This takes about 7ms
255255
element.innerHTML = finalContent;
256256

257+
var fileHeaders = element.querySelectorAll(".fileHeader a");
258+
for (var i = 0, n = fileHeaders.length; i < n; ++i) {
259+
fileHeaders[i].addEventListener("click", function(e) {
260+
e.preventDefault();
261+
toggleDiff(this.innerHTML);
262+
});
263+
}
264+
if (callbacks.binaryFileClass && callbacks.binaryFileOnClick) {
265+
var binaryFiles = element.getElementsByClassName(callbacks.binaryFileClass);
266+
for (var i = 0, n = binaryFiles.length; i < n; ++i) {
267+
binaryFiles[i].addEventListener("click", callbacks.binaryFileOnClick);
268+
}
269+
}
270+
257271
// TODO: Replace this with a performance pref call
258272
if (false)
259273
Controller.log_("Total time:" + (new Date().getTime() - start));
@@ -360,12 +374,7 @@ var inlinediff = (function () {
360374
};
361375

362376
function escape(s) {
363-
var n = s;
364-
n = n.replace(/&/g, "&amp;");
365-
n = n.replace(/</g, "&lt;");
366-
n = n.replace(/>/g, "&gt;");
367-
n = n.replace(/"/g, "&quot;");
368-
return n;
377+
return s.escapeHTML();
369378
}
370379

371380
function diffString( o, n ) {

Resources/html/views/blame/blame.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
var showFile = function(txt) {
2-
$("txt").style.display = "";
3-
$("txt").innerHTML="<pre>"+txt+"</pre>";
1+
var showFile = function(html) {
2+
var el = $("txt");
3+
el.style.display = "";
4+
el.innerHTML = "<pre>" + html + "</pre>";
5+
bindCommitSelectionLinks(el);
46

57
SyntaxHighlighter.defaults['toolbar'] = false;
68

@@ -10,4 +12,4 @@ var showFile = function(txt) {
1012

1113
var selectCommit = function(a) {
1214
Controller.selectCommit_(a);
13-
}
15+
}

Resources/html/views/commit/commit.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,18 @@ var contextLines = 0;
55

66
var showNewFile = function(file)
77
{
8-
setTitle("New file: " + file.path.escapeHTML());
8+
setTitle("New file: " + file.path);
9+
diff.innerHTML = "";
910

1011
var contents = Index.diffForFile_staged_contextLines_(file, false, contextLines);
1112
if (!contents) {
1213
notify("Can not display changes (Binary file?)", -1);
13-
diff.innerHTML = "";
1414
return;
1515
}
1616

17-
diff.innerHTML = "<pre>" + contents.escapeHTML() + "</pre>";
17+
var pre = document.createElement("pre");
18+
pre.textContent = contents;
19+
diff.appendChild(pre);
1820
diff.style.display = '';
1921
}
2022

@@ -27,11 +29,11 @@ var setState = function(state) {
2729
hideNotification();
2830
$("state").style.display = "";
2931
$("diff").style.display = "none";
30-
$("state").innerHTML = state.escapeHTML();
32+
$("state").textContent = state;
3133
}
3234

3335
var setTitle = function(status) {
34-
$("status").innerHTML = status;
36+
$("status").textContent = status;
3537
$("contextSize").style.display = "none";
3638
$("contextTitle").style.display = "none";
3739
}
@@ -59,7 +61,7 @@ var showFileChanges = function(file, cached) {
5961
if (file.status == 0) // New file?
6062
return showNewFile(file);
6163

62-
setTitle((cached ? "Staged": "Unstaged") + " changes for " + file.path.escapeHTML());
64+
setTitle((cached ? "Staged" : "Unstaged") + " changes for " + file.path);
6365
displayContext();
6466
var changes = Index.diffForFile_staged_contextLines_(file, cached, contextLines);
6567

Resources/html/views/commit/multipleSelection.js

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,18 @@ var showMultipleFilesSelection = function(files)
44
setTitle("");
55

66
var div = $("diff");
7+
div.style.display = "";
8+
div.innerHTML = '<div id="multiselect">' +
9+
'<div class="title">Multiple Selection</div>' +
10+
'<ul></ul>' +
11+
'</div>';
712

8-
var contents = '<div id="multiselect">' +
9-
'<div class="title">Multiple Selection</div>';
10-
11-
contents += "<ul>";
12-
13+
var ul = div.getElementsByTagName("ul")[0];
1314
for (var i = 0; i < files.length; ++i)
1415
{
1516
var file = files[i];
16-
contents += "<li>" + file.path + "</li>";
17+
var li = document.createElement("li");
18+
li.textContent = file.path;
19+
ul.appendChild(li);
1720
}
18-
contents += "</ul></div>";
19-
20-
div.innerHTML = contents;
21-
div.style.display = "";
22-
}
21+
}

Resources/html/views/diff/diffWindow.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
var setMessage = function(message) {
44
$("message").style.display = "";
5-
$("message").innerHTML = message.escapeHTML();
5+
$("message").textContent = message;
66
$("diff").style.display = "none";
7-
}
8-
7+
};

0 commit comments

Comments
 (0)