+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
|
# 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)
|
[![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
|
## Sanitizing HTML
|
||||||
|
|
||||||
|
@ -13,4 +14,24 @@ app.before.add(sanitizeHtmlInput());
|
||||||
|
|
||||||
// Or:
|
// Or:
|
||||||
app.chain(sanitizeHtmlInput()).get(...)
|
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.
|
/// Angel middleware designed to enhance application security.
|
||||||
library angel_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
|
name: angel_security
|
||||||
version: 0.0.0-alpha
|
version: 0.0.0-alpha+1
|
||||||
description: Angel middleware designed to enhance application security.
|
description: Angel middleware designed to enhance application security by patching common Web security holes.
|
||||||
author: Tobe O <thosakwe@gmail.com>
|
author: Tobe O <thosakwe@gmail.com>
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=1.19.0"
|
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