-
Notifications
You must be signed in to change notification settings - Fork 15
/
CSPRuleCollector.cs
226 lines (205 loc) · 7.86 KB
/
CSPRuleCollector.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using Fiddler;
namespace FiddlerCSP
{
public class CSPRuleCollector : IDisposable
{
private ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
private Dictionary<string, Dictionary<string, HashSet<string>>> rules = new Dictionary<string, Dictionary<string, HashSet<string>>>();
public delegate void RuleAddedOrModified(string uri, string rule);
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly")]
public event RuleAddedOrModified OnRuleAddedOrModified;
private ILogger logger;
public CSPRuleCollector(ILogger logger)
{
this.logger = logger;
}
public string Get(string documentUri)
{
return Get(new string[] { documentUri });
}
public string Get(string[] documentUris)
{
string prefix = "Content-Security-Policy: default-src 'none'";
string result = "";
cacheLock.EnterReadLock();
try
{
var dictsToMerge = rules.Where(x => documentUris.Contains(x.Key)).Select(x => x.Value);
var mergedDict = new Dictionary<string, HashSet<string>>();
foreach (var dict in dictsToMerge)
{
foreach (var e in dict)
{
if (!mergedDict.ContainsKey(e.Key))
{
mergedDict[e.Key] = new HashSet<string>();
}
mergedDict[e.Key] = new HashSet<string>(mergedDict[e.Key].Concat(e.Value));
}
}
result = mergedDict.OrderBy(x => x.Key).Select(entry => (
entry.Value.OrderBy(x => x).Aggregate(entry.Key, (total, next) => (total + " " + next))
)).Aggregate(prefix, (total, next) => (total + "; " + next));
}
finally
{
cacheLock.ExitReadLock();
}
return result;
}
public override string ToString()
{
string result = "";
string[] keys;
cacheLock.EnterReadLock();
try
{
keys = rules.Keys.ToArray<string>();
}
finally
{
cacheLock.ExitReadLock();
}
result = keys.Select(
uri => (uri + ": " + Get(uri))
).Aggregate("",
(total, next) => (total + "\n" + next)
) + "\n";
return result;
}
private static string UriOrigin(string fullUri)
{
var uri = fullUri;
try
{
UriBuilder uriBuilder = new UriBuilder(fullUri);
uriBuilder.Password = null;
uriBuilder.Fragment = null;
uriBuilder.Path = null;
uriBuilder.Query = null;
uriBuilder.UserName = null;
uri = uriBuilder.Uri.AbsoluteUri;
if (uri.EndsWith("/"))
{
uri = uri.Substring(0, uri.Length - 1);
}
}
catch (Exception)
{
// Ignore failure and just return the original URI.
// .NET's URI parser doesn't always match the web and we might get CSP values in
// here that aren't URIs.
}
return uri;
}
public static string UriWrtDocumentUri(string uri, string documentUri)
{
var result = uri;
try
{
var uriParsed = new Uri(uri);
var documentUriParsed = new Uri(documentUri);
if (uriParsed.Equals(documentUriParsed))
{
result = "'self'";
}
else if (uriParsed.Scheme == documentUriParsed.Scheme)
{
result = uriParsed.Host;
if (!(uriParsed.Port == 0 || uriParsed.IsDefaultPort))
{
result += ":" + uriParsed.Port;
}
}
}
catch (Exception)
{
// Again we can get exceptions if the URI can't parse as a .NET URI.
// Pass through if we can't do anything better.
}
return result;
}
public enum InterpretBlank
{
UnsafeInline,
UnsafeEval
};
public void Add(CSPReport cspReport, InterpretBlank blankIs)
{
if (cspReport.cspReport.blockedUri == null)
{
logger.Log("Invalid CSP Report - missing blocked-uri property.");
}
else if (cspReport.cspReport.documentUri == null)
{
logger.Log("Invalid CSP Report - missing document-uri property.");
}
else if (cspReport.cspReport.violatedDirective == null && cspReport.cspReport.effectiveDirective == null)
{
logger.Log("Invalid CSP Report - missing violated-directive and effective-directive properties.");
}
else
{
string documentUri = cspReport.cspReport.documentUri;
string documentUriOrigin = UriOrigin(documentUri);
string directive = cspReport.cspReport.effectiveDirective == null ? cspReport.cspReport.violatedDirective : cspReport.cspReport.effectiveDirective;
string blockedUri = cspReport.cspReport.blockedUri;
if (blockedUri.Trim().Length == 0)
{
// A blank blocked-uri indicates either unsafe-inline or unsafe-eval. The caller tells us
// which it is.
blockedUri = blankIs == InterpretBlank.UnsafeInline ? "'unsafe-inline'" : "'unsafe-eval'";
}
else if (blockedUri.IndexOf(":") >= 0) // If there's a colon, assume its a URI.
{
blockedUri = UriWrtDocumentUri(UriOrigin(blockedUri), documentUriOrigin);
}
else if (blockedUri == "self") // Firefox can return self as the blocked-uri.
{
blockedUri = "'self'";
}
else // Lastly CSP reports may contain schemes with no delimiters just the scheme name.
{
blockedUri = blockedUri + ":";
}
// Directive might be something like script-src or script-src none. We want just the first part.
directive = directive.Split(' ')[0];
cacheLock.EnterWriteLock();
try
{
if (!rules.Keys.Contains(documentUri))
{
rules.Add(documentUri, new Dictionary<string, HashSet<string>>());
}
if (!rules[documentUri].Keys.Contains(directive))
{
rules[documentUri].Add(directive, new HashSet<string>());
}
rules[documentUri][directive].Add(blockedUri);
}
finally
{
cacheLock.ExitWriteLock();
}
if (OnRuleAddedOrModified != null)
{
OnRuleAddedOrModified.Invoke(documentUri, Get(documentUri));
}
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool manageAndNativeResources)
{
cacheLock.Dispose();
}
}
}