+1
This commit is contained in:
parent
c3fa7690d1
commit
166bad95f6
7 changed files with 197 additions and 6 deletions
27
README.md
27
README.md
|
@ -1,10 +1,11 @@
|
|||
# security
|
||||
[![version 0.0.0](https://img.shields.io/badge/pub-v0.0.0--alpha-red.svg)](https://pub.dartlang.org/packages/angel_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)
|
||||
[![build status](https://travis-ci.org/angel-dart/security.svg)](https://travis-ci.org/angel-dart/security)
|
||||
|
||||
Angel middleware designed to enhance application security.
|
||||
Angel middleware designed to enhance application security by patching common Web security
|
||||
holes.
|
||||
|
||||
Currently far from finished, with incomplete code coverage - **USE AT YOUR OWN RISK!!!**
|
||||
Currently unfinished, with incomplete code coverage - **USE AT YOUR OWN RISK!!!**
|
||||
|
||||
## Sanitizing HTML
|
||||
|
||||
|
@ -13,4 +14,24 @@ app.before.add(sanitizeHtmlInput());
|
|||
|
||||
// Or:
|
||||
app.chain(sanitizeHtmlInput()).get(...)
|
||||
```
|
||||
|
||||
## CSRF Tokens
|
||||
|
||||
```dart
|
||||
app.chain(verifyCsrfToken()).post('/form', ...);
|
||||
app.responseFinalizers.add(setCsrfToken());
|
||||
```
|
||||
|
||||
## Banning IP's
|
||||
|
||||
```dart
|
||||
app.before.add(banIp('1.2.3.4'));
|
||||
|
||||
// Or a range:
|
||||
app.before.add(banIp('1.2.3.*'));
|
||||
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')]));
|
||||
```
|
|
@ -1,4 +1,6 @@
|
|||
/// Angel middleware designed to enhance application security.
|
||||
library angel_security;
|
||||
|
||||
export 'src/sanitize.dart';
|
||||
export 'src/ban.dart';
|
||||
export 'src/csrf.dart';
|
||||
export 'src/sanitize.dart';
|
||||
|
|
50
lib/src/ban.dart
Normal file
50
lib/src/ban.dart
Normal file
|
@ -0,0 +1,50 @@
|
|||
import 'dart:io';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
|
||||
/// Throws a 403 Forbidden if the user's IP is banned.
|
||||
///
|
||||
/// [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 banIp(filter,
|
||||
{String message:
|
||||
'Your IP address is forbidden from accessing this server.'}) {
|
||||
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 an IP 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 false;
|
||||
else if (input is InternetAddress && input.address == ip)
|
||||
return false;
|
||||
else if (input is String && input == ip) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!check()) throw new AngelHttpException.forbidden(message: message);
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
44
lib/src/csrf.dart
Normal file
44
lib/src/csrf.dart
Normal file
|
@ -0,0 +1,44 @@
|
|||
import 'dart:io';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
final Uuid _uuid = new Uuid();
|
||||
|
||||
/// Ensures that the request contains a correct CSRF token.
|
||||
RequestMiddleware verifyCsrfToken(
|
||||
{bool allowCookie: false,
|
||||
bool allowQuery: true,
|
||||
String name: 'csrf_token'}) {
|
||||
return (RequestContext req, res) async {
|
||||
String csrfToken;
|
||||
|
||||
if (allowQuery && req.query.containsKey(name))
|
||||
csrfToken = req.query[name];
|
||||
else if (req.body.containsKey(name))
|
||||
csrfToken = req.body[name];
|
||||
else if (allowCookie) {
|
||||
var cookie =
|
||||
req.cookies.firstWhere((c) => c.name == name, orElse: () => null);
|
||||
if (cookie != null) csrfToken = cookie.value;
|
||||
}
|
||||
|
||||
if (csrfToken == null || !req.session.containsKey(name))
|
||||
throw new AngelHttpException.badRequest(message: 'Missing CSRF token.');
|
||||
|
||||
String correctToken = req.session[name];
|
||||
|
||||
if (csrfToken != correctToken)
|
||||
throw new AngelHttpException.badRequest(message: 'Invalid CSRF token.');
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
/// Adds a CSRF token to the session, if none is present.
|
||||
RequestHandler setCsrfToken({String name: 'csrf_token', bool cookie: false}) {
|
||||
return (RequestContext req, res) async {
|
||||
if (!req.session.containsKey(name)) req.session[name] = _uuid.v4();
|
||||
if (cookie) res.cookies.add(new Cookie(name, req.session[name]));
|
||||
return true;
|
||||
};
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
name: angel_security
|
||||
version: 0.0.0-alpha
|
||||
description: Angel middleware designed to enhance application security.
|
||||
version: 0.0.0-alpha+1
|
||||
description: Angel middleware designed to enhance application security by patching common Web security holes.
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
environment:
|
||||
sdk: ">=1.19.0"
|
||||
|
|
24
test/ban.dart
Normal file
24
test/ban.dart
Normal file
|
@ -0,0 +1,24 @@
|
|||
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);
|
||||
});
|
||||
}
|
50
test/csrf_token.dart
Normal file
50
test/csrf_token.dart
Normal file
|
@ -0,0 +1,50 @@
|
|||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_security/angel_security.dart';
|
||||
import 'package:angel_test/angel_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
final RegExp _sessId = new RegExp(r'DARTSESSID=([^;]+);');
|
||||
|
||||
main() async {
|
||||
Angel app;
|
||||
TestClient client;
|
||||
|
||||
setUp(() async {
|
||||
app = new Angel()..responseFinalizers.add(setCsrfToken());
|
||||
|
||||
app.chain(verifyCsrfToken()).get('/valid', 'Valid!');
|
||||
|
||||
client = await connectTo(app);
|
||||
});
|
||||
|
||||
tearDown(() => client.close());
|
||||
|
||||
test('need pre-existing token', () async {
|
||||
var response = await client.get('/valid?csrf_token=evil');
|
||||
print(response.body);
|
||||
expect(response, hasStatus(400));
|
||||
expect(response.body, contains('Missing'));
|
||||
});
|
||||
|
||||
test('fake token', () async {
|
||||
// Get a valid CSRF, but ignore it.
|
||||
var response = await client.get('/');
|
||||
var sessionId = getCookie(response);
|
||||
response = await client.get('/valid?csrf_token=evil',
|
||||
headers: {'cookie': 'DARTSESSID=$sessionId'});
|
||||
print(response.body);
|
||||
expect(response, hasStatus(400));
|
||||
expect(response.body.contains('Valid'), isFalse);
|
||||
expect(response.body, contains('Invalid CSRF token'));
|
||||
});
|
||||
}
|
||||
|
||||
String getCookie(http.Response response) {
|
||||
if (response.headers.containsKey('set-cookie')) {
|
||||
var header = response.headers['set-cookie'];
|
||||
var match = _sessId.firstMatch(header);
|
||||
return match?.group(1);
|
||||
} else
|
||||
return null;
|
||||
}
|
Loading…
Reference in a new issue