This commit is contained in:
thosakwe 2017-01-13 19:45:35 -05:00
parent 166bad95f6
commit 75a96a4bab
13 changed files with 407 additions and 28 deletions

View file

@ -1,5 +1,5 @@
# security
[![version 0.0.0-alpha+1](https://img.shields.io/badge/pub-v0.0.0--alpha+1-red.svg)](https://pub.dartlang.org/packages/angel_security)
[![version 0.0.0-alpha+2](https://img.shields.io/badge/pub-v0.0.0--alpha+2-red.svg)](https://pub.dartlang.org/packages/angel_security)
[![build status](https://travis-ci.org/angel-dart/security.svg)](https://travis-ci.org/angel-dart/security)
Angel middleware designed to enhance application security by patching common Web security
@ -7,6 +7,15 @@ holes.
Currently unfinished, with incomplete code coverage - **USE AT YOUR OWN RISK!!!**
* Generic Middleware
* [Sanitizing HTML](#sanitizing-html)
* [CSRF Tokens](#csrf-tokens)
* [Banning by IP/Origin](#banning-by-ip)
* [Trusted Proxy](#trusted-proxy)
* [Throttling Requests](#throttling-requests)
* [Helmet Port](#helmet)
* [Service Hooks](#service-hooks)
## Sanitizing HTML
```dart
@ -23,7 +32,7 @@ app.chain(verifyCsrfToken()).post('/form', ...);
app.responseFinalizers.add(setCsrfToken());
```
## Banning IP's
## Banning by IP
```dart
app.before.add(banIp('1.2.3.4'));
@ -34,4 +43,43 @@ app.before.add(banIp('1.2.*.4'));
// Or multiple filters:
app.before.add(banIp(['1.2.3.4', '192.*.*.*', new RegExp(r'1\.2.\3.\4')]));
// Also can ban origins
app.before.add(banOrigin('*.known-attacker.com'));
// By default, `banOrigin` forces users to have an `Origin` header.
// Use this flag to disable it:
app.before.add(banOrigin('evil.site', allowEmptyOrigin: true));
```
## Trusted Proxy
Works well with Apache or Nginx.
```dart
// ONLY trust localhost X-Forwarded-* headers
app.before.add(trustProxy('127.0.0.1'));
```
## Throttling Requests
Throws a `429` error if the given rate limit is exceeded.
```dart
// Example: 5 requests per minute
app.before.add(throttleRequests(5, new Duration(minutes: 1)));
# Helmet
`security` includes a port of [`helmetjs`](https://github.com/helmetjs/helmet).
Helmet includes 11 middleware that attempt to enhance security via HTTP headers.
Call `helmet` to include all of them.
```dart
import 'package:angel_security/helmet.dart';
```
# Service Hooks
Also included are a set of service hooks, [ported from FeathersJS](https://github.com/feathersjs/feathers-legacy-authentication-hooks).
```dart
import 'package:angel_security/hooks.dart';
```

View file

@ -4,3 +4,5 @@ library angel_security;
export 'src/ban.dart';
export 'src/csrf.dart';
export 'src/sanitize.dart';
export 'src/throttle.dart';
export 'src/trust_proxy.dart';

48
lib/helmet.dart Normal file
View file

@ -0,0 +1,48 @@
library angel_security.helmet;
import 'package:angel_framework/angel_framework.dart';
/// A collection of 11 middleware and handlers to help secure your application
/// using HTTP headers.
AngelConfigurer helmet() {
throw new Exception("Helmet isn't ready just yet! ;)");
return (Angel app) async {
app.before.add(waterfall([
contentSecurityPolicy(),
dnsPrefetchControl(),
frameguard(),
hpkp(),
hsts(),
ieNoOpen(),
noCache(),
noSniff(),
referrerPolicy(),
xssFilter()
]));
app.responseFinalizers.addAll([hidePoweredBy]);
};
}
RequestMiddleware contentSecurityPolicy() {}
RequestMiddleware dnsPrefetchControl() {}
RequestMiddleware frameguard() {}
RequestMiddleware hidePoweredBy() {}
RequestMiddleware hpkp() {}
RequestMiddleware hsts() {}
RequestMiddleware ieNoOpen() {}
RequestMiddleware noCache() {}
RequestMiddleware noSniff() {}
RequestMiddleware referrerPolicy() {}
RequestMiddleware xssFilter() {}

2
lib/hooks.dart Normal file
View file

@ -0,0 +1,2 @@
/// Coming soon!
library angel_security.hooks;

View file

@ -44,7 +44,55 @@ RequestMiddleware banIp(filter,
}
if (!check()) throw new AngelHttpException.forbidden(message: message);
return true;
};
}
/// Throws a 403 Forbidden if the user's Origin header is banned.
///
/// [filter] can be:
/// A `String`, `RegExp`, or an `Iterable`.
///
/// String can take the following formats:
/// 1. example.com
/// 2. *.example.com, a.b.*.d.e.f, etc.
RequestMiddleware banOrigin(filter,
{String message: 'You are forbidden from accessing this server.',
bool allowEmptyOrigin: false}) {
var filters = [];
Iterable inputs = filter is Iterable ? filter : [filter];
for (var input in inputs) {
if (input is RegExp)
filters.add(input);
else if (input is String) {
if (!input.contains('*'))
filters.add(input);
else {
filters.add(new RegExp(input.replaceAll('*', '[^\.]+')));
}
} else
throw new ArgumentError('Cannot use $input as an origin filter.');
}
return (RequestContext req, ResponseContext res) async {
var origin = req.headers.value('origin');
if ((origin == null || origin.isEmpty) && !allowEmptyOrigin)
throw new AngelHttpException.badRequest(
message: "'Origin' header is required.");
bool check() {
for (var input in filters) {
if (input is RegExp && input.hasMatch(origin))
return false;
else if (input is String && input == origin) return false;
}
return true;
}
if (!check()) throw new AngelHttpException.forbidden(message: message);
return true;
};
}

54
lib/src/throttle.dart Normal file
View file

@ -0,0 +1,54 @@
import 'package:angel_framework/angel_framework.dart';
/// Prevents users from sending more than a given [max] number of
/// requests within a given [duration].
///
/// Use [identify] to create a unique identifier for each request.
/// The default is to identify requests by their source IP.
///
/// This works well attached to a `multiserver` instance.
RequestMiddleware throttleRequests(int max, Duration duration,
{String message: '429 Too Many Requests', identify(RequestContext req)}) {
var identifyRequest = identify ?? (RequestContext req) async => req.ip;
Map<String, int> table = {};
Map<String, List<int>> times = {};
return (RequestContext req, ResponseContext res) async {
var id = (await identifyRequest(req)).toString();
int currentCount;
var now = new DateTime.now().millisecondsSinceEpoch;
int firstVisit;
// If the user has visited within the given duration...
if (times.containsKey(id)) {
firstVisit = times[id].first;
}
// If difference in times is greater than duration, reset counter ;)
if (firstVisit != null) {
if (now - firstVisit > duration.inMilliseconds) {
table.remove(id);
times.remove(id);
}
}
// Save to time table
if (times.containsKey(id))
times[id].add(now);
else
times[id] = [now];
if (table.containsKey(id))
currentCount = table[id] = table[id] + 1;
else
currentCount = table[id] = 1;
if (currentCount > max) {
throw new AngelHttpException(null,
statusCode: 429, message: message ?? '429 Too Many Requests');
}
return true;
};
}

82
lib/src/trust_proxy.dart Normal file
View file

@ -0,0 +1,82 @@
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
/// Injects a [ForwardedClient] if the user comes from a
/// trusted proxy.
///
/// [filter] can be:
/// A `String`, `RegExp`, `InternetAddress`, or an `Iterable`.
///
/// String can take the following formats:
/// 1. 1.2.3.4
/// 2. 1.2.3.*, 1.2.*.*, etc.
RequestMiddleware trustProxy(filter) {
var filters = [];
Iterable inputs = filter is Iterable ? filter : [filter];
for (var input in inputs) {
if (input is RegExp || input is InternetAddress)
filters.add(input);
else if (input is String) {
if (!input.contains('*'))
filters.add(input);
else {
filters.add(new RegExp(input.replaceAll('*', '[0-9]+')));
}
} else
throw new ArgumentError('Cannot use $input as a trusted proxy filter.');
}
return (RequestContext req, ResponseContext res) async {
var ip = req.ip;
bool check() {
for (var input in filters) {
if (input is RegExp && input.hasMatch(ip))
return true;
else if (input is InternetAddress && input.address == ip)
return true;
else if (input is String && input == ip) return true;
}
return false;
}
if (check()) {
Map<String, List<String>> headers = {};
req.headers.forEach((k, v) {
if (k.trim().toLowerCase().startsWith('x-forwarded')) headers[k] = v;
});
req.inject(ForwardedClient, new _ForwardedClientImpl(headers));
}
return true;
};
}
/// Presents information about the client forwarded by a trusted
/// reverse proxy.
abstract class ForwardedClient {
Map<String, List<String>> get headers;
String get ip => headers['x-forwarded-for']?.join(',');
String get host => headers['x-forwarded-host']?.join(',');
String get protocol => headers['x-forwarded-proto']?.join(',');
int get port {
var portString = headers['x-forwarded-proto']?.join(',');
return portString != null ? int.parse(portString) : null;
}
}
class _ForwardedClientImpl extends ForwardedClient {
final Map<String, List<String>> _headers;
_ForwardedClientImpl(this._headers);
@override
Map<String, List<String>> get headers =>
new Map<String, List<String>>.unmodifiable(_headers);
}

View file

@ -1,5 +1,5 @@
name: angel_security
version: 0.0.0-alpha+1
version: 0.0.0-alpha+2
description: Angel middleware designed to enhance application security by patching common Web security holes.
author: Tobe O <thosakwe@gmail.com>
environment:

View file

@ -1,24 +0,0 @@
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_security/angel_security.dart';
import 'package:angel_test/angel_test.dart';
import 'package:test/test.dart';
main() {
Angel app;
TestClient client;
setUp(() async {
app = new Angel()..chain(banIp('*.*.*.*')).get('/ban', 'WTF');
client = await connectTo(app);
});
tearDown(() => client.close());
test('ban everyone', () async {
var response = await client.get('/ban');
print(response.body);
expect(response, hasStatus(403));
expect(response.body.contains('WTF'), isFalse);
});
}

51
test/ban_test.dart Normal file
View file

@ -0,0 +1,51 @@
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_security/angel_security.dart';
import 'package:angel_test/angel_test.dart';
import 'package:test/test.dart';
main() {
Angel app;
TestClient client;
setUp(() async {
app = new Angel()
..chain(banIp('*.*.*.*')).get('/ban', 'WTF')
..chain(banOrigin('*')).get('/ban-origin', 'WTF')
..chain(banOrigin('*.foo.bar')).get('/allow-origin', 'YAY');
client = await connectTo(app);
});
tearDown(() => client.close());
test('ban everyone', () async {
var response = await client.get('/ban');
print(response.body);
expect(response, hasStatus(403));
expect(response.body.contains('WTF'), isFalse);
});
group('origin', () {
test('ban everyone', () async {
var response = await client
.get('/ban-origin', headers: {'Origin': 'www.example.com'});
print(response.body);
expect(response, hasStatus(403));
expect(response.body.contains('WTF'), isFalse);
});
test('ban specific', () async {
var response =
await client.get('/allow-origin', headers: {'Origin': 'www.foo.bar'});
print(response.body);
expect(response, hasStatus(403));
expect(response.body.contains('YAY'), isFalse);
response = await client
.get('/allow-origin', headers: {'Origin': 'www.example.com'});
print(response.body);
expect(response, hasStatus(200));
expect(response.body, contains('YAY'));
});
});
}

33
test/throttle_test.dart Normal file
View file

@ -0,0 +1,33 @@
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_security/angel_security.dart';
import 'package:angel_test/angel_test.dart';
import 'package:test/test.dart';
main() {
Angel app;
TestClient client;
setUp(() async {
app = new Angel();
app
.chain(throttleRequests(1, new Duration(hours: 1)))
.get('/once-per-hour', 'OK');
client = await connectTo(app);
});
tearDown(() => client.close());
test('enforce limit', () async {
// First request within the hour is fine
var response = await client.get('/once-per-hour');
print(response.body);
expect(response.body, contains('OK'));
// Second request within an hour? No no no!
response = await client.get('/once-per-hour');
print(response.body);
expect(response, hasStatus(429));
});
}

View file

@ -0,0 +1,35 @@
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_security/angel_security.dart';
import 'package:angel_test/angel_test.dart';
import 'package:test/test.dart';
verifyProxy(RequestContext req) =>
req.injections.containsKey(ForwardedClient) ? 'Yep' : 'Nope';
main() {
Angel app;
TestClient client;
setUp(() async {
app = new Angel()
..chain(trustProxy('127.*.*.*')).get('/hello', verifyProxy)
..chain(trustProxy('1.2.3.4')).get('/foo', verifyProxy);
client = await connectTo(app);
});
tearDown(() => client.close());
test('wildcard', () async {
var response =
await client.get('/hello', headers: {'X-Forwarded-Host': 'foo'});
print(response.body);
expect(response.body, contains('Yep'));
});
test('exclude unknown', () async {
var response =
await client.get('/foo', headers: {'X-Forwarded-Host': 'foo'});
print(response.body);
expect(response.body, contains('Nope'));
});
}