From 166bad95f63b5ce870096ce8ec1cedba929ab852 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Thu, 12 Jan 2017 22:11:55 -0500 Subject: [PATCH] +1 --- README.md | 27 +++++++++++++++++++--- lib/angel_security.dart | 4 +++- lib/src/ban.dart | 50 +++++++++++++++++++++++++++++++++++++++++ lib/src/csrf.dart | 44 ++++++++++++++++++++++++++++++++++++ pubspec.yaml | 4 ++-- test/ban.dart | 24 ++++++++++++++++++++ test/csrf_token.dart | 50 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 197 insertions(+), 6 deletions(-) create mode 100644 lib/src/ban.dart create mode 100644 lib/src/csrf.dart create mode 100644 test/ban.dart create mode 100644 test/csrf_token.dart diff --git a/README.md b/README.md index 329c2ea0..63141d18 100644 --- a/README.md +++ b/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')])); ``` \ No newline at end of file diff --git a/lib/angel_security.dart b/lib/angel_security.dart index 08722d75..35547707 100644 --- a/lib/angel_security.dart +++ b/lib/angel_security.dart @@ -1,4 +1,6 @@ /// Angel middleware designed to enhance application security. library angel_security; -export 'src/sanitize.dart'; \ No newline at end of file +export 'src/ban.dart'; +export 'src/csrf.dart'; +export 'src/sanitize.dart'; diff --git a/lib/src/ban.dart b/lib/src/ban.dart new file mode 100644 index 00000000..2c6592c4 --- /dev/null +++ b/lib/src/ban.dart @@ -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; + }; +} diff --git a/lib/src/csrf.dart b/lib/src/csrf.dart new file mode 100644 index 00000000..c6ebe0ba --- /dev/null +++ b/lib/src/csrf.dart @@ -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; + }; +} diff --git a/pubspec.yaml b/pubspec.yaml index 8dd8edcd..43135033 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 environment: sdk: ">=1.19.0" diff --git a/test/ban.dart b/test/ban.dart new file mode 100644 index 00000000..73c0d9bb --- /dev/null +++ b/test/ban.dart @@ -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); + }); +} diff --git a/test/csrf_token.dart b/test/csrf_token.dart new file mode 100644 index 00000000..245d1e89 --- /dev/null +++ b/test/csrf_token.dart @@ -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; +}