diff --git a/SafeExamBrowser.Browser.UnitTests/Filters/RequestFilterTests.cs b/SafeExamBrowser.Browser.UnitTests/Filters/RequestFilterTests.cs
new file mode 100644
index 00000000..039e158d
--- /dev/null
+++ b/SafeExamBrowser.Browser.UnitTests/Filters/RequestFilterTests.cs
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
+ * 
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using SafeExamBrowser.Browser.Filters;
+using SafeExamBrowser.Settings.Browser;
+
+namespace SafeExamBrowser.Browser.UnitTests.Filters
+{
+	[TestClass]
+	public class RequestFilterTests
+	{
+		private RequestFilter sut;
+
+		[TestInitialize]
+		public void Initialize()
+		{
+			sut = new RequestFilter();
+		}
+
+		[TestMethod]
+		public void MustProcessBlockRulesFirst()
+		{
+			var allow = new FilterRuleSettings { Expression = "*", Type = FilterType.Simplified, Result = FilterResult.Allow };
+			var block = new FilterRuleSettings { Expression = "*", Type = FilterType.Simplified, Result = FilterResult.Block };
+
+			sut.Load(allow);
+			sut.Load(block);
+
+			var result = sut.Process("safeexambrowser.org");
+
+			Assert.AreEqual(FilterResult.Block, result);
+		}
+
+		[TestMethod]
+		public void MustProcessAllowRulesSecond()
+		{
+			var allow = new FilterRuleSettings { Expression = "*", Type = FilterType.Simplified, Result = FilterResult.Allow };
+			var block = new FilterRuleSettings { Expression = "xyz", Type = FilterType.Simplified, Result = FilterResult.Block };
+
+			sut.Load(allow);
+			sut.Load(block);
+
+			var result = sut.Process("safeexambrowser.org");
+
+			Assert.AreEqual(FilterResult.Allow, result);
+		}
+
+		[TestMethod]
+		public void MustReturnDefault()
+		{
+			var allow = new FilterRuleSettings { Expression = "xyz", Type = FilterType.Simplified, Result = FilterResult.Allow };
+			var block = new FilterRuleSettings { Expression = "xyz", Type = FilterType.Simplified, Result = FilterResult.Block };
+
+			sut.Default = (FilterResult) (-1);
+			sut.Load(allow);
+			sut.Load(block);
+
+			var result = sut.Process("safeexambrowser.org");
+
+			Assert.AreEqual((FilterResult) (-1), result);
+		}
+
+		[TestMethod]
+		public void MustReturnDefaultWithoutRules()
+		{
+			sut.Default = FilterResult.Allow;
+			var result = sut.Process("safeexambrowser.org");
+			Assert.AreEqual(FilterResult.Allow, result);
+
+			sut.Default = FilterResult.Block;
+			result = sut.Process("safeexambrowser.org");
+			Assert.AreEqual(FilterResult.Block, result);
+		}
+
+		[TestMethod]
+		[ExpectedException(typeof(NotImplementedException))]
+		public void MustNotAllowUnsupportedResult()
+		{
+			sut.Load(new FilterRuleSettings { Result = (FilterResult) (-1) });
+		}
+
+		[TestMethod]
+		[ExpectedException(typeof(NotImplementedException))]
+		public void MustNotAllowUnsupportedFilterType()
+		{
+			sut.Load(new FilterRuleSettings { Type = (FilterType) (-1) });
+		}
+	}
+}
diff --git a/SafeExamBrowser.Browser.UnitTests/Properties/AssemblyInfo.cs b/SafeExamBrowser.Browser.UnitTests/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..7c1bb291
--- /dev/null
+++ b/SafeExamBrowser.Browser.UnitTests/Properties/AssemblyInfo.cs
@@ -0,0 +1,17 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+[assembly: AssemblyTitle("SafeExamBrowser.Browser.UnitTests")]
+[assembly: AssemblyDescription("Safe Exam Browser")]
+[assembly: AssemblyCompany("ETH Zürich")]
+[assembly: AssemblyProduct("SafeExamBrowser.Browser.UnitTests")]
+[assembly: AssemblyCopyright("Copyright © 2019 ETH Zürich, Educational Development and Technology (LET)")]
+
+[assembly: ComVisible(false)]
+
+[assembly: Guid("f54c4c0e-4c72-4f88-a389-7f6de3ccb745")]
+
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
+[assembly: AssemblyInformationalVersion("1.0.0.0")]
diff --git a/SafeExamBrowser.Browser.UnitTests/SafeExamBrowser.Browser.UnitTests.csproj b/SafeExamBrowser.Browser.UnitTests/SafeExamBrowser.Browser.UnitTests.csproj
new file mode 100644
index 00000000..ff793437
--- /dev/null
+++ b/SafeExamBrowser.Browser.UnitTests/SafeExamBrowser.Browser.UnitTests.csproj
@@ -0,0 +1,100 @@
+
+
+  
+  
+  
+    Debug
+    AnyCPU
+    {F54C4C0E-4C72-4F88-A389-7F6DE3CCB745}
+    Library
+    Properties
+    SafeExamBrowser.Browser.UnitTests
+    SafeExamBrowser.Browser.UnitTests
+    v4.7.2
+    512
+    {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+    15.0
+    $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+    $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages
+    False
+    UnitTest
+    
+    
+  
+  
+    true
+    full
+    false
+    bin\Debug\
+    DEBUG;TRACE
+    prompt
+    4
+  
+  
+    pdbonly
+    true
+    bin\Release\
+    TRACE
+    prompt
+    4
+  
+  
+    true
+    bin\x86\Debug\
+    DEBUG;TRACE
+    full
+    x86
+    prompt
+    MinimumRecommendedRules.ruleset
+  
+  
+    bin\x86\Release\
+    TRACE
+    true
+    pdbonly
+    x86
+    prompt
+    MinimumRecommendedRules.ruleset
+  
+  
+    
+      ..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll
+    
+    
+      ..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll
+    
+    
+    
+  
+  
+    
+    
+  
+  
+    
+  
+  
+    
+      {5fb5273d-277c-41dd-8593-a25ce1aff2e9}
+      SafeExamBrowser.Browser.Contracts
+    
+    
+      {04e653f1-98e6-4e34-9dd7-7f2bc1a8b767}
+      SafeExamBrowser.Browser
+    
+    
+      {30b2d907-5861-4f39-abad-c4abf1b3470e}
+      SafeExamBrowser.Settings
+    
+  
+  
+  
+  
+    
+      This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them.  For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
+    
+    
+    
+  
+  
+
\ No newline at end of file
diff --git a/SafeExamBrowser.Browser.UnitTests/packages.config b/SafeExamBrowser.Browser.UnitTests/packages.config
new file mode 100644
index 00000000..2f7c5a18
--- /dev/null
+++ b/SafeExamBrowser.Browser.UnitTests/packages.config
@@ -0,0 +1,5 @@
+
+
+  
+  
+
\ No newline at end of file
diff --git a/SafeExamBrowser.Browser/BrowserApplicationInstance.cs b/SafeExamBrowser.Browser/BrowserApplicationInstance.cs
index 61e81cf1..259f2dda 100644
--- a/SafeExamBrowser.Browser/BrowserApplicationInstance.cs
+++ b/SafeExamBrowser.Browser/BrowserApplicationInstance.cs
@@ -14,9 +14,9 @@ using SafeExamBrowser.Browser.Contracts.Events;
 using SafeExamBrowser.Browser.Events;
 using SafeExamBrowser.Browser.Handlers;
 using SafeExamBrowser.Configuration.Contracts;
-using SafeExamBrowser.Settings.Browser;
 using SafeExamBrowser.I18n.Contracts;
 using SafeExamBrowser.Logging.Contracts;
+using SafeExamBrowser.Settings.Browser;
 using SafeExamBrowser.UserInterface.Contracts;
 using SafeExamBrowser.UserInterface.Contracts.Browser;
 using SafeExamBrowser.UserInterface.Contracts.MessageBox;
@@ -102,7 +102,7 @@ namespace SafeExamBrowser.Browser
 			var keyboardHandler = new KeyboardHandler();
 			var lifeSpanHandler = new LifeSpanHandler();
 			var requestLogger = logger.CloneFor($"{nameof(RequestHandler)} {Id}");
-			var requestHandler = new RequestHandler(appConfig, settings, logger);
+			var requestHandler = new RequestHandler(appConfig, settings.Filter, logger, text);
 
 			displayHandler.FaviconChanged += DisplayHandler_FaviconChanged;
 			displayHandler.ProgressChanged += DisplayHandler_ProgressChanged;
@@ -117,6 +117,8 @@ namespace SafeExamBrowser.Browser
 			control.AddressChanged += Control_AddressChanged;
 			control.LoadingStateChanged += Control_LoadingStateChanged;
 			control.TitleChanged += Control_TitleChanged;
+
+			requestHandler.Initiailize();
 			control.Initialize();
 
 			logger.Debug("Initialized browser control.");
diff --git a/SafeExamBrowser.Browser/Filters/BlockedContent.html b/SafeExamBrowser.Browser/Filters/BlockedContent.html
new file mode 100644
index 00000000..f73bea4e
--- /dev/null
+++ b/SafeExamBrowser.Browser/Filters/BlockedContent.html
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/SafeExamBrowser.Browser/Filters/BlockedPage.html b/SafeExamBrowser.Browser/Filters/BlockedPage.html
new file mode 100644
index 00000000..f9dad271
--- /dev/null
+++ b/SafeExamBrowser.Browser/Filters/BlockedPage.html
@@ -0,0 +1,13 @@
+
+
+    
+        
+        %%TITLE%%
+    
+    
+        
+            
%%MESSAGE%%
+            
+        
 
+    
+
\ No newline at end of file
diff --git a/SafeExamBrowser.Browser/Filters/RequestFilter.cs b/SafeExamBrowser.Browser/Filters/RequestFilter.cs
index cb84e344..6cec7142 100644
--- a/SafeExamBrowser.Browser/Filters/RequestFilter.cs
+++ b/SafeExamBrowser.Browser/Filters/RequestFilter.cs
@@ -7,20 +7,74 @@
  */
 
 using System;
+using System.Collections.Generic;
+using SafeExamBrowser.Browser.Filters.Rules;
 using SafeExamBrowser.Settings.Browser;
 
 namespace SafeExamBrowser.Browser.Filters
 {
 	internal class RequestFilter
 	{
-		internal void Load(FilterRule rule)
-		{
+		private IList allowRules;
+		private IList blockRules;
 
+		internal FilterResult Default { get; set; }
+
+		internal RequestFilter()
+		{
+			allowRules = new List();
+			blockRules = new List();
+			Default = FilterResult.Block;
 		}
 
-		internal FilterResult Process(Uri request)
+		internal void Load(FilterRuleSettings settings)
 		{
-			return FilterResult.Allow;
+			var rule = default(Rule);
+
+			switch (settings.Type)
+			{
+				case FilterType.Regex:
+					rule = new RegexRule(settings.Expression);
+					break;
+				case FilterType.Simplified:
+					rule = new SimpleRule(settings.Expression);
+					break;
+				default:
+					throw new NotImplementedException($"Filter rule of type '{settings.Type}' is not yet implemented!");
+			}
+
+			switch (settings.Result)
+			{
+				case FilterResult.Allow:
+					allowRules.Add(rule);
+					break;
+				case FilterResult.Block:
+					blockRules.Add(rule);
+					break;
+				default:
+					throw new NotImplementedException($"Filter result '{settings.Result}' is not yet implemented!");
+			}
+		}
+
+		internal FilterResult Process(string url)
+		{
+			foreach (var rule in blockRules)
+			{
+				if (rule.IsMatch(url))
+				{
+					return FilterResult.Block;
+				}
+			}
+
+			foreach (var rule in allowRules)
+			{
+				if (rule.IsMatch(url))
+				{
+					return FilterResult.Allow;
+				}
+			}
+
+			return Default;
 		}
 	}
 }
diff --git a/SafeExamBrowser.Browser/Filters/Rules/RegexRule.cs b/SafeExamBrowser.Browser/Filters/Rules/RegexRule.cs
new file mode 100644
index 00000000..d8bf6989
--- /dev/null
+++ b/SafeExamBrowser.Browser/Filters/Rules/RegexRule.cs
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
+ * 
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+using System.Text.RegularExpressions;
+
+namespace SafeExamBrowser.Browser.Filters.Rules
+{
+	internal class RegexRule : Rule
+	{
+		private string expression;
+
+		public RegexRule(string expression) : base(expression)
+		{
+		}
+
+		protected override void Initialize(string expression)
+		{
+			this.expression = expression;
+		}
+
+		internal override bool IsMatch(string url)
+		{
+			return Regex.IsMatch(url, expression, RegexOptions.IgnoreCase);
+		}
+	}
+}
diff --git a/SafeExamBrowser.Browser/Filters/Rules/Rule.cs b/SafeExamBrowser.Browser/Filters/Rules/Rule.cs
new file mode 100644
index 00000000..7c0a85c3
--- /dev/null
+++ b/SafeExamBrowser.Browser/Filters/Rules/Rule.cs
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
+ * 
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+namespace SafeExamBrowser.Browser.Filters.Rules
+{
+	internal abstract class Rule
+	{
+		internal Rule(string expression)
+		{
+			Initialize(expression);
+		}
+
+		internal abstract bool IsMatch(string url);
+		protected abstract void Initialize(string expression);
+	}
+}
diff --git a/SafeExamBrowser.Browser/Filters/Rules/SimpleRule.cs b/SafeExamBrowser.Browser/Filters/Rules/SimpleRule.cs
new file mode 100644
index 00000000..ccf793a1
--- /dev/null
+++ b/SafeExamBrowser.Browser/Filters/Rules/SimpleRule.cs
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
+ * 
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+using System.Text.RegularExpressions;
+
+namespace SafeExamBrowser.Browser.Filters.Rules
+{
+	internal class SimpleRule : Rule
+	{
+		private string expression;
+
+		public SimpleRule(string expression) : base(expression)
+		{
+		}
+
+		protected override void Initialize(string expression)
+		{
+			this.expression = expression.Replace("*", @".*");
+		}
+
+		internal override bool IsMatch(string url)
+		{
+			return Regex.IsMatch(url, expression, RegexOptions.IgnoreCase);
+		}
+	}
+}
diff --git a/SafeExamBrowser.Browser/Handlers/RequestHandler.cs b/SafeExamBrowser.Browser/Handlers/RequestHandler.cs
index 78dc4ffe..60369c30 100644
--- a/SafeExamBrowser.Browser/Handlers/RequestHandler.cs
+++ b/SafeExamBrowser.Browser/Handlers/RequestHandler.cs
@@ -7,46 +7,30 @@
  */
 
 using CefSharp;
-using SafeExamBrowser.Browser.Filters;
 using SafeExamBrowser.Configuration.Contracts;
+using SafeExamBrowser.I18n.Contracts;
 using SafeExamBrowser.Logging.Contracts;
-using BrowserSettings = SafeExamBrowser.Settings.Browser.BrowserSettings;
+using SafeExamBrowser.Settings.Browser;
 
 namespace SafeExamBrowser.Browser.Handlers
 {
 	internal class RequestHandler : CefSharp.Handler.RequestHandler
 	{
-		private AppConfig appConfig;
-		private BrowserSettings settings;
-		private RequestFilter filter;
-		private ILogger logger;
-		private ResourceRequestHandler resourceRequestHandler;
+		private ResourceHandler resourceHandler;
 
-		internal RequestHandler(AppConfig appConfig, BrowserSettings settings, ILogger logger)
+		internal RequestHandler(AppConfig appConfig, BrowserFilterSettings settings, ILogger logger, IText text)
 		{
-			this.appConfig = appConfig;
-			this.settings = settings;
-			this.filter = new RequestFilter();
-			this.logger = logger;
-			this.resourceRequestHandler = new ResourceRequestHandler(appConfig, settings, logger);
+			this.resourceHandler = new ResourceHandler(appConfig, settings, logger, text);
 		}
 
 		internal void Initiailize()
 		{
-			if (settings.FilterMainRequests || settings.FilterContentRequests)
-			{
-				foreach (var rule in settings.FilterRules)
-				{
-					filter.Load(rule);
-				}
-
-				logger.Debug("Initialized request filter.");
-			}
+			resourceHandler.Initialize();
 		}
 
-		protected override IResourceRequestHandler GetResourceRequestHandler(IWebBrowser webBrowser, IBrowser browser, IFrame frame, IRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling)
+		protected override IResourceRequestHandler GetResourceRequestHandler(IWebBrowser webBrowser, IBrowser browser, IFrame frame, IRequest request, 	bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling)
 		{
-			return resourceRequestHandler;
+			return resourceHandler;
 		}
 	}
 }
diff --git a/SafeExamBrowser.Browser/Handlers/ResourceHandler.cs b/SafeExamBrowser.Browser/Handlers/ResourceHandler.cs
new file mode 100644
index 00000000..ac86d6bc
--- /dev/null
+++ b/SafeExamBrowser.Browser/Handlers/ResourceHandler.cs
@@ -0,0 +1,174 @@
+/*
+ * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
+ * 
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+using System;
+using System.Collections.Specialized;
+using System.IO;
+using System.Reflection;
+using CefSharp;
+using SafeExamBrowser.Browser.Filters;
+using SafeExamBrowser.Configuration.Contracts;
+using SafeExamBrowser.I18n.Contracts;
+using SafeExamBrowser.Logging.Contracts;
+using SafeExamBrowser.Settings.Browser;
+
+namespace SafeExamBrowser.Browser.Handlers
+{
+	internal class ResourceHandler : CefSharp.Handler.ResourceRequestHandler
+	{
+		private AppConfig appConfig;
+		private BrowserFilterSettings settings;
+		private ILogger logger;
+		private RequestFilter filter;
+		private IResourceHandler contentBlockedHandler;
+		private IResourceHandler pageBlockedHandler;
+		private IText text;
+
+		internal ResourceHandler(AppConfig appConfig, BrowserFilterSettings settings, ILogger logger, IText text)
+		{
+			this.appConfig = appConfig;
+			this.filter = new RequestFilter();
+			this.logger = logger;
+			this.settings = settings;
+			this.text = text;
+		}
+
+		internal void Initialize()
+		{
+			if (settings.FilterMainRequests || settings.FilterContentRequests)
+			{
+				InitializeFilter();
+			}
+		}
+
+		protected override IResourceHandler GetResourceHandler(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request)
+		{
+			if (BlockMainRequest(request))
+			{
+				return pageBlockedHandler;
+			}
+
+			if (BlockContentRequest(request))
+			{
+				return contentBlockedHandler;
+			}
+
+			return base.GetResourceHandler(chromiumWebBrowser, browser, frame, request);
+		}
+
+		protected override CefReturnValue OnBeforeResourceLoad(IWebBrowser webBrowser, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback)
+		{
+			// TODO: CEF does not yet support intercepting requests from service workers, thus the user agent must be statically set at browser
+			//       startup for now. Once CEF has full support of service workers, the static user agent should be removed and the method below
+			//       reactivated. See https://bitbucket.org/chromiumembedded/cef/issues/2622 for the current status of development.
+			// AppendCustomUserAgent(request);
+
+			if (IsMailtoUrl(request.Url))
+			{
+				return CefReturnValue.Cancel;
+			}
+
+			ReplaceCustomScheme(request);
+
+			return base.OnBeforeResourceLoad(webBrowser, browser, frame, request, callback);
+		}
+
+		private void AppendCustomUserAgent(IRequest request)
+		{
+			var headers = new NameValueCollection(request.Headers);
+			var userAgent = request.Headers["User-Agent"];
+
+			headers["User-Agent"] = $"{userAgent} SEB/{appConfig.ProgramInformationalVersion}";
+			request.Headers = headers;
+		}
+
+		private bool BlockContentRequest(IRequest request)
+		{
+			if (settings.FilterContentRequests && request.ResourceType != ResourceType.MainFrame)
+			{
+				var result = filter.Process(request.Url);
+				var block = result == FilterResult.Block;
+
+				if (block)
+				{
+					logger.Info($"Blocked content request for '{request.Url}'.");
+				}
+
+				return block;
+			}
+
+			return false;
+		}
+
+		private bool BlockMainRequest(IRequest request)
+		{
+			if (settings.FilterMainRequests && request.ResourceType == ResourceType.MainFrame)
+			{
+				var result = filter.Process(request.Url);
+				var block = result == FilterResult.Block;
+
+				if (block)
+				{
+					logger.Info($"Blocked main request for '{request.Url}'.");
+				}
+
+				return block;
+			}
+
+			return false;
+		}
+
+		private void InitializeFilter()
+		{
+			var assembly = Assembly.GetAssembly(typeof(RequestFilter));
+			var contentMessage = text.Get(TextKey.Browser_BlockedContentMessage);
+			var contentStream = assembly.GetManifestResourceStream($"{typeof(RequestFilter).Namespace}.BlockedContent.html");
+			var pageButton = text.Get(TextKey.Browser_BlockedPageButton);
+			var pageMessage = text.Get(TextKey.Browser_BlockedPageMessage);
+			var pageTitle = text.Get(TextKey.Browser_BlockedPageTitle);
+			var pageStream = assembly.GetManifestResourceStream($"{typeof(RequestFilter).Namespace}.BlockedPage.html");
+			var contentHtml = new StreamReader(contentStream).ReadToEnd();
+			var pageHtml = new StreamReader(pageStream).ReadToEnd();
+
+			contentHtml = contentHtml.Replace("%%MESSAGE%%", contentMessage);
+			pageHtml = pageHtml.Replace("%%MESSAGE%%", pageMessage).Replace("%%TITLE%%", pageTitle).Replace("%%BACK_BUTTON%%", pageButton);
+
+			contentBlockedHandler = CefSharp.ResourceHandler.FromString(contentHtml);
+			pageBlockedHandler = CefSharp.ResourceHandler.FromString(pageHtml);
+
+			foreach (var rule in settings.Rules)
+			{
+				filter.Load(rule);
+			}
+
+			logger.Debug($"Initialized request filter with {settings.Rules.Count} rules.");
+		}
+
+		private bool IsMailtoUrl(string url)
+		{
+			return url.StartsWith(Uri.UriSchemeMailto);
+		}
+
+		private void ReplaceCustomScheme(IRequest request)
+		{
+			if (Uri.IsWellFormedUriString(request.Url, UriKind.RelativeOrAbsolute))
+			{
+				var uri = new Uri(request.Url);
+
+				if (uri.Scheme == appConfig.SebUriScheme)
+				{
+					request.Url = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttp }.Uri.AbsoluteUri;
+				}
+				else if (uri.Scheme == appConfig.SebUriSchemeSecure)
+				{
+					request.Url = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttps }.Uri.AbsoluteUri;
+				}
+			}
+		}
+	}
+}
diff --git a/SafeExamBrowser.Browser/Handlers/ResourceRequestHandler.cs b/SafeExamBrowser.Browser/Handlers/ResourceRequestHandler.cs
deleted file mode 100644
index 35897e24..00000000
--- a/SafeExamBrowser.Browser/Handlers/ResourceRequestHandler.cs
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
- * 
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/.
- */
-
-using System;
-using System.Collections.Specialized;
-using CefSharp;
-using SafeExamBrowser.Configuration.Contracts;
-using SafeExamBrowser.Logging.Contracts;
-using BrowserSettings = SafeExamBrowser.Settings.Browser.BrowserSettings;
-
-namespace SafeExamBrowser.Browser.Handlers
-{
-	internal class ResourceRequestHandler : CefSharp.Handler.ResourceRequestHandler
-	{
-		private AppConfig appConfig;
-		private BrowserSettings settings;
-		private ILogger logger;
-
-		internal ResourceRequestHandler(AppConfig appConfig, BrowserSettings settings, ILogger logger)
-		{
-			this.appConfig = appConfig;
-			this.settings = settings;
-			this.logger = logger;
-		}
-
-		protected override IResourceHandler GetResourceHandler(IWebBrowser webBrowser, IBrowser browser, IFrame frame, IRequest request)
-		{
-			if (FilterMainRequest(request) || FilterContentRequest(request))
-			{
-				return ResourceHandler.FromString("Blocked!");
-			}
-
-			return base.GetResourceHandler(webBrowser, browser, frame, request);
-		}
-
-		protected override CefReturnValue OnBeforeResourceLoad(IWebBrowser webBrowser, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback)
-		{
-			// TODO: CEF does not yet support intercepting requests from service workers, thus the user agent must be statically set at browser
-			//       startup for now. Once CEF has full support of service workers, the static user agent should be removed and the method below
-			//       reactivated. See https://bitbucket.org/chromiumembedded/cef/issues/2622 for the current status of development.
-			// AppendCustomUserAgent(request);
-
-			if (IsMailtoUrl(request.Url))
-			{
-				return CefReturnValue.Cancel;
-			}
-
-			ReplaceCustomScheme(request);
-
-			return base.OnBeforeResourceLoad(webBrowser, browser, frame, request, callback);
-		}
-
-		private void AppendCustomUserAgent(IRequest request)
-		{
-			var headers = new NameValueCollection(request.Headers);
-			var userAgent = request.Headers["User-Agent"];
-
-			headers["User-Agent"] = $"{userAgent} SEB/{appConfig.ProgramInformationalVersion}";
-			request.Headers = headers;
-		}
-
-		private bool FilterContentRequest(IRequest request)
-		{
-			return settings.FilterContentRequests && request.ResourceType != ResourceType.MainFrame;
-		}
-
-		private bool FilterMainRequest(IRequest request)
-		{
-			return settings.FilterMainRequests && request.ResourceType == ResourceType.MainFrame;
-		}
-
-		private bool IsMailtoUrl(string url)
-		{
-			return url.StartsWith(Uri.UriSchemeMailto);
-		}
-
-		private void ReplaceCustomScheme(IRequest request)
-		{
-			if (Uri.IsWellFormedUriString(request.Url, UriKind.RelativeOrAbsolute))
-			{
-				var uri = new Uri(request.Url);
-
-				if (uri.Scheme == appConfig.SebUriScheme)
-				{
-					request.Url = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttp }.Uri.AbsoluteUri;
-				}
-				else if (uri.Scheme == appConfig.SebUriSchemeSecure)
-				{
-					request.Url = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttps }.Uri.AbsoluteUri;
-				}
-			}
-		}
-	}
-}
diff --git a/SafeExamBrowser.Browser/Properties/AssemblyInfo.cs b/SafeExamBrowser.Browser/Properties/AssemblyInfo.cs
index e827a509..357b5ac6 100644
--- a/SafeExamBrowser.Browser/Properties/AssemblyInfo.cs
+++ b/SafeExamBrowser.Browser/Properties/AssemblyInfo.cs
@@ -1,4 +1,5 @@
 using System.Reflection;
+using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
 
 // General Information about an assembly is controlled through the following
@@ -14,6 +15,7 @@ using System.Runtime.InteropServices;
 // to COM components.  If you need to access a type in this assembly from
 // COM, set the ComVisible attribute to true on that type.
 [assembly: ComVisible(false)]
+[assembly: InternalsVisibleTo("SafeExamBrowser.Browser.UnitTests")]
 
 // The following GUID is for the ID of the typelib if this project is exposed to COM
 [assembly: Guid("04e653f1-98e6-4e34-9dd7-7f2bc1a8b767")]
diff --git a/SafeExamBrowser.Browser/SafeExamBrowser.Browser.csproj b/SafeExamBrowser.Browser/SafeExamBrowser.Browser.csproj
index 253bda8c..93c88d9a 100644
--- a/SafeExamBrowser.Browser/SafeExamBrowser.Browser.csproj
+++ b/SafeExamBrowser.Browser/SafeExamBrowser.Browser.csproj
@@ -59,6 +59,7 @@
   
   
     
+    
     
     
     
@@ -72,6 +73,9 @@
     
     
     
+    
+    
+    
     
     
       Component
@@ -82,7 +86,7 @@
     
     
     
-    
+    
     
   
   
@@ -124,6 +128,10 @@
       Designer
     
   
+  
+    
+    
+  
   
   
     
diff --git a/SafeExamBrowser.Configuration/ConfigurationData/DataMapper.Browser.cs b/SafeExamBrowser.Configuration/ConfigurationData/DataMapper.Browser.cs
index 9579c2ac..4cabd897 100644
--- a/SafeExamBrowser.Configuration/ConfigurationData/DataMapper.Browser.cs
+++ b/SafeExamBrowser.Configuration/ConfigurationData/DataMapper.Browser.cs
@@ -110,7 +110,7 @@ namespace SafeExamBrowser.Configuration.ConfigurationData
 		{
 			if (value is bool filter)
 			{
-				settings.Browser.FilterContentRequests = filter;
+				settings.Browser.Filter.FilterContentRequests = filter;
 			}
 		}
 
@@ -118,7 +118,7 @@ namespace SafeExamBrowser.Configuration.ConfigurationData
 		{
 			if (value is bool filter)
 			{
-				settings.Browser.FilterMainRequests = filter;
+				settings.Browser.Filter.FilterMainRequests = filter;
 			}
 		}
 
@@ -126,30 +126,33 @@ namespace SafeExamBrowser.Configuration.ConfigurationData
 		{
 			const int ALLOW = 1;
 
-			if (value is IEnumerable> ruleDataList)
+			if (value is IList