Skip to content

Commit f8a6ba7

Browse files
authored
Merge pull request #124 from nanotech/xss
Fix and mitigate code injection (XSS) in HTML views
2 parents a92319c + 8ca76ad commit f8a6ba7

24 files changed

+389
-229
lines changed

Classes/Views/GLFileView.h

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,15 @@
1717
__weak IBOutlet PBGitHistoryController* historyController;
1818
__weak IBOutlet MGScopeBar *typeBar;
1919
NSMutableArray *groups;
20-
NSString *logFormat;
2120
__weak IBOutlet NSView *accessoryView;
2221
__weak IBOutlet NSSplitView *fileListSplitView;
2322
}
2423

2524
- (void)showFile;
2625
- (void)didLoad;
2726
- (NSString *)parseBlame:(NSString *)txt;
28-
- (NSString *)parseHTML:(NSString *)txt;
27+
- (NSString *)escapeHTML:(NSString *)txt;
2928

3029
@property NSMutableArray *groups;
31-
@property NSString *logFormat;
3230

3331
@end

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/css/GitX.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,7 @@ table {
3131
font-family: Menlo, Monaco, monospace;
3232
text-decoration: none;
3333
}
34+
35+
.hidden {
36+
display: none;
37+
}

Resources/html/lib/GitX.js

Lines changed: 28 additions & 17 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,25 +33,36 @@ 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");
38-
n.style.display = "";
39-
$("notification_message").innerHTML = text;
38+
n.classList.remove("hidden");
39+
$("notification_message").innerHTML = html;
4040

4141
// Change color
4242
if (!state) { // Busy
43-
$("spinner").style.display = "";
44-
n.setAttribute("class", "");
43+
$("spinner").classList.remove("hidden");
44+
n.classList.remove("success");
45+
n.classList.remove("fail");
4546
}
4647
else if (state == 1) { // Success
47-
$("spinner").style.display = "none";
48-
n.setAttribute("class", "success");
48+
$("spinner").classList.add("hidden");
49+
n.classList.add("success");
4950
} else if (state == -1) {// Fail
50-
$("spinner").style.display = "none";
51-
n.setAttribute("class", "fail");
51+
$("spinner").classList.add("hidden");
52+
n.classList.add("fail");
5253
}
5354
}
5455

5556
var hideNotification = function() {
56-
$("notification").style.display = "none";
57+
$("notification").classList.add("hidden");
5758
}
59+
60+
var bindCommitSelectionLinks = function(el) {
61+
var links = el.getElementsByClassName("commit-link");
62+
for (var i = 0, n = links.length; i < n; ++i) {
63+
links[i].addEventListener("click", function(e) {
64+
e.preventDefault();
65+
selectCommit(this.dataset.commitId || this.innerHTML);
66+
}, false);
67+
}
68+
};

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/blame/index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<html>
22
<head>
3+
<meta charset="utf-8">
4+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src 'self';">
35
<script src="../../lib/GitX.js" type="text/javascript" charset="utf-8"></script>
46
<script src="../../lib/syntaxhighlighter/scripts/shCore.js" type="text/javascript" charset="utf-8"></script>
57
<script src="../../lib/syntaxhighlighter/scripts/shBrushObjC.js" type="text/javascript" charset="utf-8"></script>
@@ -11,4 +13,4 @@
1113
<body>
1214
<div id="txt">hola</pre>
1315
</body>
14-
</html>
16+
</html>

0 commit comments

Comments
 (0)