@@ -23,6 +23,16 @@ Duration timeoutDelay = Duration(seconds: isTesting ? 1 : 8);
2323/// The keys we currently allow the url to be signed with.
2424Map <int , Uint8List > allowedKeys = {};
2525
26+ // Inspired by https://github.com/atmos/camo/blob/master/server.coffee#L39.
27+ Map <String , String > securityHeaders = {
28+ 'X-Frame-Options' : 'deny' ,
29+ 'X-XSS-Protection' : '1; mode=block' ,
30+ 'X-Content-Type-Options' : 'nosniff' ,
31+ 'Content-Security-Policy' :
32+ "default-src 'none'; img-src data:; style-src 'unsafe-inline'" ,
33+ 'Strict-Transport-Security' : 'max-age=31536000; includeSubDomains' ,
34+ };
35+
2636/// Ensure that [allowedKeys] contains keys for today and the two surrounding
2737/// days.
2838Future <void > updateAllowedKeys () async {
@@ -104,13 +114,17 @@ final maxImageSize = 1024 * 1024 * 10; // At most 10 MB.
104114Future <shelf.Response > handler (shelf.Request request) async {
105115 try {
106116 if (request.method != 'GET' ) {
107- return shelf.Response .notFound ('Unsupported method' );
117+ return shelf.Response .notFound (
118+ 'Unsupported method' ,
119+ headers: securityHeaders,
120+ );
108121 }
109122 final segments = request.url.pathSegments;
110123 if (segments.length != 3 ) {
111124 return shelf.Response .badRequest (
112125 body:
113126 'malformed request, ${segments .length } should be of the form <base64(hmac(url,daily_secret))>/<date>/<urlencode(url)>' ,
127+ headers: securityHeaders,
114128 );
115129 }
116130 final Uint8List signature;
@@ -119,22 +133,30 @@ Future<shelf.Response> handler(shelf.Request request) async {
119133 } on FormatException catch (_) {
120134 return shelf.Response .badRequest (
121135 body: 'malformed request, could not decode mac signature' ,
136+ headers: securityHeaders,
122137 );
123138 }
124139 final date = int .tryParse (segments[1 ]);
125140 if (date == null ) {
126- return shelf.Response .badRequest (body: 'malformed request, missing date' );
141+ return shelf.Response .badRequest (
142+ body: 'malformed request, missing date' ,
143+ headers: securityHeaders,
144+ );
127145 }
128146 final secret = allowedKeys[date];
129147 if (secret == null ) {
130148 return shelf.Response .badRequest (
131149 body: 'malformed request, proxy url expired' ,
150+ headers: securityHeaders,
132151 );
133152 }
134153
135154 final imageUrl = segments[2 ];
136155 if (imageUrl.length > 1024 ) {
137- return shelf.Response .badRequest (body: 'proxied url too long' );
156+ return shelf.Response .badRequest (
157+ body: 'proxied url too long' ,
158+ headers: securityHeaders,
159+ );
138160 }
139161 final imageUrlBytes = utf8.encode (imageUrl);
140162
@@ -143,16 +165,23 @@ Future<shelf.Response> handler(shelf.Request request) async {
143165 try {
144166 parsedImageUrl = Uri .parse (imageUrl);
145167 } on FormatException catch (e) {
146- return shelf.Response .badRequest (body: 'Malformed proxied url $e ' );
168+ return shelf.Response .badRequest (
169+ body: 'Malformed proxied url $e ' ,
170+ headers: securityHeaders,
171+ );
147172 }
148173 if (! (parsedImageUrl.isScheme ('http' ) ||
149174 parsedImageUrl.isScheme ('https' ))) {
150175 return shelf.Response .badRequest (
151176 body: 'Can only proxy http and https urls' ,
177+ headers: securityHeaders,
152178 );
153179 }
154180 if (! parsedImageUrl.isAbsolute) {
155- return shelf.Response .badRequest (body: 'Can only proxy absolute urls' );
181+ return shelf.Response .badRequest (
182+ body: 'Can only proxy absolute urls' ,
183+ headers: securityHeaders,
184+ );
156185 }
157186
158187 int statusCode;
@@ -233,14 +262,24 @@ Future<shelf.Response> handler(shelf.Request request) async {
233262 e is ServerSideException ,
234263 );
235264 } on TooLargeException {
236- return shelf.Response .badRequest (body: 'Image too large' );
265+ return shelf.Response .badRequest (
266+ body: 'Image too large' ,
267+ headers: securityHeaders,
268+ );
237269 } on RedirectException catch (e) {
238- return shelf.Response .badRequest (body: e.message);
270+ return shelf.Response .badRequest (
271+ body: e.message,
272+ headers: securityHeaders,
273+ );
239274 } on RequestTimeoutException catch (e) {
240- return shelf.Response .badRequest (body: e.message);
275+ return shelf.Response .badRequest (
276+ body: e.message,
277+ headers: securityHeaders,
278+ );
241279 } on ServerSideException catch (e) {
242280 return shelf.Response .badRequest (
243281 body: 'Failed to retrieve image. Status code ${e .statusCode }' ,
282+ headers: securityHeaders,
244283 );
245284 }
246285
@@ -251,10 +290,11 @@ Future<shelf.Response> handler(shelf.Request request) async {
251290 'Cache-control' : 'max-age=180, public' ,
252291 'content-type' : ? contentType,
253292 'content-encoding' : ? contentEncoding,
293+ ...securityHeaders,
254294 },
255295 );
256296 } else {
257- return shelf.Response .unauthorized ('Bad hmac' );
297+ return shelf.Response .unauthorized ('Bad hmac' , headers : securityHeaders );
258298 }
259299 } catch (e, st) {
260300 stderr.writeln ('Uncaught error: $e $st ' );
0 commit comments