CVE-2017-18016: Origin Validation Error (CWE-346)
value: Parity Browser 1.6.10 and earlier allows remote attackers to bypass the Same Origin Policy and obtain sensitive information by requesting other websites via the Parity web proxy engine (reusing the current website's token, which is not bound to an origin).
CWE description: The product does not properly verify that the source of data or communication is valid.
CVSS v3 base score: 5.3
Published: 2018-01-11T16:29Z
Repository: https://github.com/paritytech/parity
Language: Rust
Files changed: dapps/src/lib.rs, dapps/src/tests/fetch.rs, dapps/src/tests/helpers/mod.rs, dapps/src/web.rs, js/src/api/rpc/signer/signer.js, js/src/jsonrpc/interfaces/signer.js, js/src/views/Web/store.js, js/src/views/Web/store.spec.js, js/src/views/Web/web.js, parity/dapps.rs, rpc/src/v1/helpers/signer.rs, rpc/src/v1/impls/signer.rs, rpc/src/v1/traits/signer.rs
Commit date: 2017-06-22 20:05:40 +0200
Commit message: Domain-locked web tokens. (#5894)

* Domain-locking web tokens.

* JS part.

* Fix linting issues.

Diff: diff --git a/dapps/src/lib.rs b/dapps/src/lib.rs
index 0860f0c10..1cbc446a7 100644
--- a/dapps/src/lib.rs
+++ b/dapps/src/lib.rs
@@ -69,11 +69,11 @@ mod tests;
 
 use std::path::PathBuf;
 use std::sync::Arc;
 use std::collections::HashMap;
 
-use jsonrpc_http_server::{self as http, hyper};
+use jsonrpc_http_server::{self as http, hyper, Origin};
 
 use fetch::Fetch;
 use parity_reactor::Remote;
 
 pub use hash_fetch::urlhint::ContractClient;
@@ -88,16 +88,16 @@ impl<F> SyncStatus for F where F: Fn() -> bool + Send + Sync {
 	fn is_major_importing(&self) -> bool { self() }
 }
 
 /// Validates Web Proxy tokens
 pub trait WebProxyTokens: Send + Sync {
-	/// Should return true if token is a valid web proxy access token.
-	fn is_web_proxy_token_valid(&self, token: &str) -> bool;
+	/// Should return a domain allowed to be accessed by this token or `None` if the token is not valid
+	fn domain(&self, token: &str) -> Option<Origin>;
 }
 
-impl<F> WebProxyTokens for F where F: Fn(String) -> bool + Send + Sync {
-	fn is_web_proxy_token_valid(&self, token: &str) -> bool { self(token.to_owned()) }
+impl<F> WebProxyTokens for F where F: Fn(String) -> Option<Origin> + Send + Sync {
+	fn domain(&self, token: &str) -> Option<Origin> { self(token.to_owned()) }
 }
 
 /// Current supported endpoints.
 pub struct Endpoints {
 	endpoints: endpoint::Endpoints,
diff --git a/dapps/src/tests/fetch.rs b/dapps/src/tests/fetch.rs
index 7a9773f07..51650a963 100644
--- a/dapps/src/tests/fetch.rs
+++ b/dapps/src/tests/fetch.rs
@@ -310,11 +310,11 @@ fn should_encode_and_decode_base32() {
 }
 
 #[test]
 fn should_stream_web_content() {
 	// given
-	let (server, fetch) = serve_with_fetch("token");
+	let (server, fetch) = serve_with_fetch("token", "https://parity.io");
 
 	// when
 	let response = request(server,
 		"\
 			GET / HTTP/1.1\r\n\
@@ -333,11 +333,11 @@ fn should_stream_web_content() {
 }
 
 #[test]
 fn should_support_base32_encoded_web_urls() {
 	// given
-	let (server, fetch) = serve_with_fetch("token");
+	let (server, fetch) = serve_with_fetch("token", "https://parity.io");
 
 	// when
 	let response = request(server,
 		"\
 			GET /styles.css?test=123 HTTP/1.1\r\n\
@@ -356,11 +356,11 @@ fn should_support_base32_encoded_web_urls() {
 }
 
 #[test]
 fn should_correctly_handle_long_label_when_splitted() {
 	// given
-	let (server, fetch) = serve_with_fetch("xolrg9fePeQyKLnL");
+	let (server, fetch) = serve_with_fetch("xolrg9fePeQyKLnL", "https://contribution.melonport.com");
 
 	// when
 	let response = request(server,
 		"\
 			GET /styles.css?test=123 HTTP/1.1\r\n\
@@ -380,11 +380,11 @@ fn should_correctly_handle_long_label_when_splitted() {
 
 
 #[test]
 fn should_support_base32_encoded_web_urls_as_path() {
 	// given
-	let (server, fetch) = serve_with_fetch("token");
+	let (server, fetch) = serve_with_fetch("token", "https://parity.io");
 
 	// when
 	let response = request(server,
 		"\
 			GET /web/EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY/styles.css?test=123 HTTP/1.1\r\n\
@@ -400,14 +400,36 @@ fn should_support_base32_encoded_web_urls_as_path() {
 
 	fetch.assert_requested("https://parity.io/styles.css?test=123");
 	fetch.assert_no_more_requests();
 }
 
+#[test]
+fn should_return_error_on_non_whitelisted_domain() {
+	// given
+	let (server, fetch) = serve_with_fetch("token", "https://ethcore.io");
+
+	// when
+	let response = request(server,
+		"\
+			GET / HTTP/1.1\r\n\
+			Host: EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY.web.web3.site\r\n\
+			Connection: close\r\n\
+			\r\n\
+		"
+	);
+
+	// then
+	response.assert_status("HTTP/1.1 400 Bad Request");
+	assert_security_headers_for_embed(&response.headers);
+
+	fetch.assert_no_more_requests();
+}
+
 #[test]
 fn should_return_error_on_invalid_token() {
 	// given
-	let (server, fetch) = serve_with_fetch("test");
+	let (server, fetch) = serve_with_fetch("test", "https://parity.io");
 
 	// when
 	let response = request(server,
 		"\
 			GET / HTTP/1.1\r\n\
@@ -425,11 +447,11 @@ fn should_return_error_on_invalid_token() {
 }
 
 #[test]
 fn should_return_error_on_invalid_protocol() {
 	// given
-	let (server, fetch) = serve_with_fetch("token");
+	let (server, fetch) = serve_with_fetch("token", "ftp://parity.io");
 
 	// when
 	let response = request(server,
 		"\
 			GET /web/token/ftp/parity.io/ HTTP/1.1\r\n\
@@ -447,11 +469,11 @@ fn should_return_error_on_invalid_protocol() {
 }
 
 #[test]
 fn should_disallow_non_get_requests() {
 	// given
-	let (server, fetch) = serve_with_fetch("token");
+	let (server, fetch) = serve_with_fetch("token", "https://parity.io");
 
 	// when
 	let response = request(server,
 		"\
 			POST / HTTP/1.1\r\n\
@@ -472,11 +494,11 @@ fn should_disallow_non_get_requests() {
 }
 
 #[test]
 fn should_fix_absolute_requests_based_on_referer() {
 	// given
-	let (server, fetch) = serve_with_fetch("token");
+	let (server, fetch) = serve_with_fetch("token", "https://parity.io");
 
 	// when
 	let response = request(server,
 		"\
 			GET /styles.css HTTP/1.1\r\n\
@@ -495,11 +517,11 @@ fn should_fix_absolute_requests_based_on_referer() {
 }
 
 #[test]
 fn should_fix_absolute_requests_based_on_referer_in_url() {
 	// given
-	let (server, fetch) = serve_with_fetch("token");
+	let (server, fetch) = serve_with_fetch("token", "https://parity.io");
 
 	// when
 	let response = request(server,
 		"\
 			GET /styles.css HTTP/1.1\r\n\
diff --git a/dapps/src/tests/helpers/mod.rs b/dapps/src/tests/helpers/mod.rs
index 6bc0006ce..060420792 100644
--- a/dapps/src/tests/helpers/mod.rs
+++ b/dapps/src/tests/helpers/mod.rs
@@ -98,17 +98,19 @@ pub fn serve_with_registrar_and_fetch_and_threads(multi_threaded: bool) -> (Serv
 	}, Default::default(), if multi_threaded { Remote::new_thread_per_future() } else { Remote::new_sync() });
 
 	(server, fetch, reg)
 }
 
-pub fn serve_with_fetch(web_token: &'static str) -> (Server, FakeFetch) {
+pub fn serve_with_fetch(web_token: &'static str, domain: &'static str) -> (Server, FakeFetch) {
 	let fetch = FakeFetch::default();
 	let f = fetch.clone();
 	let (server, _) = init_server(move |builder| {
 		builder
 			.fetch(f.clone())
-			.web_proxy_tokens(Arc::new(move |token| &token == web_token))
+			.web_proxy_tokens(Arc::new(move |token| {
+				if &token == web_token { Some(domain.into()) } else { None }
+			}))
 	}, Default::default(), Remote::new_sync());
 
 	(server, fetch)
 }
 
@@ -145,11 +147,11 @@ impl ServerBuilder {
 	pub fn new<P: AsRef<Path>>(dapps_path: P, registrar: Arc<ContractClient>, remote: Remote) -> Self {
 		ServerBuilder {
 			dapps_path: dapps_path.as_ref().to_owned(),
 			registrar: registrar,
 			sync_status: Arc::new(|| false),
-			web_proxy_tokens: Arc::new(|_| false),
+			web_proxy_tokens: Arc::new(|_| None),
 			signer_address: None,
 			allowed_hosts: DomainsValidation::Disabled,
 			remote: remote,
 			fetch: None,
 		}
diff --git a/dapps/src/web.rs b/dapps/src/web.rs
index 0e872daf2..a404099db 100644
--- a/dapps/src/web.rs
+++ b/dapps/src/web.rs
@@ -131,18 +131,18 @@ impl<F: Fetch> WebHandler<F> {
 		let mut token_it = token_and_url.split('+');
 		let token = token_it.next();
 		let target_url = token_it.next();
 
 		// Check if token supplied in URL is correct.
-		match token {
-			Some(token) if self.web_proxy_tokens.is_web_proxy_token_valid(token) => {},
+		let domain = match token.and_then(|token| self.web_proxy_tokens.domain(token)) {
+			Some(domain) => domain,
 			_ => {
 				return Err(State::Error(ContentHandler::error(
 					StatusCode::BadRequest, "Invalid Access Token", "Invalid or old web proxy access token supplied.", Some("Try refreshing the page."), self.embeddable_on.clone()
 				)));
 			}
-		}
+		};
 
 		// Validate protocol
 		let mut target_url = match target_url {
 			Some(url) if url.starts_with("http://") || url.starts_with("https://") => url.to_owned(),
 			_ => {
@@ -150,10 +150,16 @@ impl<F: Fetch> WebHandler<F> {
 					StatusCode::BadRequest, "Invalid Protocol", "Invalid protocol used.", None, self.embeddable_on.clone()
 				)));
 			}
 		};
 
+		if !target_url.starts_with(&*domain) {
+			return Err(State::Error(ContentHandler::error(
+				StatusCode::BadRequest, "Invalid Domain", "Dapp attempted to access invalid domain.", Some(&target_url), self.embeddable_on.clone(),
+			)));
+		}
+
 		if !target_url.ends_with("/") {
 			target_url = format!("{}/", target_url);
 		}
 
 		// TODO [ToDr] Should just use `path.app_params`
diff --git a/js/src/api/rpc/signer/signer.js b/js/src/api/rpc/signer/signer.js
index 3beef5747..915b81158 100644
--- a/js/src/api/rpc/signer/signer.js
+++ b/js/src/api/rpc/signer/signer.js
@@ -40,13 +40,13 @@ export default class Signer {
   generateAuthorizationToken () {
     return this._transport
       .execute('signer_generateAuthorizationToken');
   }
 
-  generateWebProxyAccessToken () {
+  generateWebProxyAccessToken (domain) {
     return this._transport
-      .execute('signer_generateWebProxyAccessToken');
+      .execute('signer_generateWebProxyAccessToken', domain);
   }
 
   rejectRequest (requestId) {
     return this._transport
       .execute('signer_rejectRequest', inNumber16(requestId));
diff --git a/js/src/jsonrpc/interfaces/signer.js b/js/src/jsonrpc/interfaces/signer.js
index 495b8e0e8..ead97df16 100644
--- a/js/src/jsonrpc/interfaces/signer.js
+++ b/js/src/jsonrpc/interfaces/signer.js
@@ -28,11 +28,15 @@ export default {
     }
   },
 
   generateWebProxyAccessToken: {
     desc: 'Generates a new web proxy access token.',
-    params: [],
+    params: [{
+      type: String,
+      desc: 'Domain for which the token is valid. Only requests to this domain will be allowed.',
+      example: 'https://parity.io'
+    }],
     returns: {
       type: String,
       desc: 'The new web proxy access token.',
       example: 'MOWm0tEJjwthDiTU'
     }
diff --git a/js/src/views/Web/store.js b/js/src/views/Web/store.js
index 4eed3eaf3..9dd8ea3fe 100644
--- a/js/src/views/Web/store.js
+++ b/js/src/views/Web/store.js
@@ -57,19 +57,21 @@ export default class Store {
   @computed get isPristine () {
     return this.currentUrl === this.nextUrl;
   }
 
   @action gotoUrl = (_url) => {
-    transaction(() => {
-      let url = (_url || this.nextUrl).trim().replace(/\/+$/, '');
+    let url = (_url || this.nextUrl).trim().replace(/\/+$/, '');
 
-      if (!hasProtocol.test(url)) {
-        url = `https://${url}`;
-      }
+    if (!hasProtocol.test(url)) {
+      url = `https://${url}`;
+    }
 
-      this.setNextUrl(url);
-      this.setCurrentUrl(this.nextUrl);
+    return this.generateToken(url).then(() => {
+      transaction(() => {
+        this.setNextUrl(url);
+        this.setCurrentUrl(this.nextUrl);
+      });
     });
   }
 
   @action reload = () => {
     transaction(() => {
@@ -132,15 +134,15 @@ export default class Store {
 
   @action setNextUrl = (url) => {
     this.nextUrl = url;
   }
 
-  generateToken = () => {
+  generateToken = (_url) => {
     this.setToken(null);
 
     return this._api.signer
-      .generateWebProxyAccessToken()
+      .generateWebProxyAccessToken(_url)
       .then((token) => {
         this.setToken(token);
       })
       .catch((error) => {
         console.warn('generateToken', error);
diff --git a/js/src/views/Web/store.spec.js b/js/src/views/Web/store.spec.js
index 9f7d2e777..8e92cbd01 100644
--- a/js/src/views/Web/store.spec.js
+++ b/js/src/views/Web/store.spec.js
@@ -60,19 +60,20 @@ describe('views/Web/Store', () => {
 
   describe('@action', () => {
     describe('gotoUrl', () => {
       it('uses the nextUrl when none specified', () => {
         store.setNextUrl('https://parity.io');
-        store.gotoUrl();
 
-        expect(store.currentUrl).to.equal('https://parity.io');
+        return store.gotoUrl().then(() => {
+          expect(store.currentUrl).to.equal('https://parity.io');
+        });
       });
 
       it('adds https when no protocol', () => {
-        store.gotoUrl('google.com');
-
-        expect(store.currentUrl).to.equal('https://google.com');
+        return store.gotoUrl('google.com').then(() => {
+          expect(store.currentUrl).to.equal('https://google.com');
+        });
       });
     });
 
     describe('restoreUrl', () => {
       it('sets the nextUrl to the currentUrl', () => {
diff --git a/js/src/views/Web/web.js b/js/src/views/Web/web.js
index 2d94b1b16..8e81b43d5 100644
--- a/js/src/views/Web/web.js
+++ b/js/src/views/Web/web.js
@@ -35,11 +35,10 @@ export default class Web extends Component {
 
   store = Store.get(this.context.api);
 
   componentDidMount () {
     this.store.gotoUrl(this.props.params.url);
-    return this.store.generateToken();
   }
 
   componentWillReceiveProps (props) {
     this.store.gotoUrl(props.params.url);
   }
diff --git a/parity/dapps.rs b/parity/dapps.rs
index f0ae06c6b..b19eaebb6 100644
--- a/parity/dapps.rs
+++ b/parity/dapps.rs
@@ -230,11 +230,11 @@ mod server {
 		extra_dapps: Vec<PathBuf>,
 		dapps_domain: String,
 	) -> Result<Middleware, String> {
 		let signer = deps.signer;
 		let parity_remote = parity_reactor::Remote::new(deps.remote.clone());
-		let web_proxy_tokens = Arc::new(move |token| signer.is_valid_web_proxy_access_token(&token));
+		let web_proxy_tokens = Arc::new(move |token| signer.web_proxy_access_token_domain(&token));
 
 		Ok(parity_dapps::Middleware::dapps(
 			parity_remote,
 			deps.ui_address,
 			dapps_path,
diff --git a/rpc/src/v1/helpers/signer.rs b/rpc/src/v1/helpers/signer.rs
index f35832a40..81d21eb82 100644
--- a/rpc/src/v1/helpers/signer.rs
+++ b/rpc/src/v1/helpers/signer.rs
@@ -14,10 +14,11 @@
 // You should have received a copy of the GNU General Public License
 // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 
 use std::sync::Arc;
 use std::ops::Deref;
+use http::Origin;
 use util::Mutex;
 use transient_hashmap::TransientHashMap;
 
 use ethstore::random_string;
 
@@ -27,11 +28,11 @@ const TOKEN_LIFETIME_SECS: u32 = 3600;
 
 /// Manages communication with Signer crate
 pub struct SignerService {
 	is_enabled: bool,
 	queue: Arc<ConfirmationsQueue>,
-	web_proxy_tokens: Mutex<TransientHashMap<String, ()>>,
+	web_proxy_tokens: Mutex<TransientHashMap<String, Origin>>,
 	generate_new_token: Box<Fn() -> Result<String, String> + Send + Sync + 'static>,
 }
 
 impl SignerService {
 	/// Creates new Signer Service given function to generate new tokens.
@@ -44,20 +45,20 @@ impl SignerService {
 			is_enabled: is_enabled,
 		}
 	}
 
 	/// Checks if the token is valid web proxy access token.
-	pub fn is_valid_web_proxy_access_token(&self, token: &String) -> bool {
-		self.web_proxy_tokens.lock().contains_key(&token)
+	pub fn web_proxy_access_token_domain(&self, token: &String) -> Option<Origin> {
+		self.web_proxy_tokens.lock().get(token).cloned()
 	}
 
 	/// Generates a new web proxy access token.
-	pub fn generate_web_proxy_access_token(&self) -> String {
+	pub fn generate_web_proxy_access_token(&self, domain: Origin) -> String {
 		let token = random_string(16);
 		let mut tokens = self.web_proxy_tokens.lock();
 		tokens.prune();
-		tokens.insert(token.clone(), ());
+		tokens.insert(token.clone(), domain);
 		token
 	}
 
 	/// Generates new signer authorization token.
 	pub fn generate_token(&self) -> Result<String, String> {
diff --git a/rpc/src/v1/impls/signer.rs b/rpc/src/v1/impls/signer.rs
index 4667283c9..d0b1b7a3c 100644
--- a/rpc/src/v1/impls/signer.rs
+++ b/rpc/src/v1/impls/signer.rs
@@ -243,12 +243,12 @@ impl<D: Dispatcher + 'static> Signer for SignerClient<D> {
 	fn generate_token(&self) -> Result<String, Error> {
 		self.signer.generate_token()
 			.map_err(|e| errors::token(e))
 	}
 
-	fn generate_web_proxy_token(&self) -> Result<String, Error> {
-		Ok(self.signer.generate_web_proxy_access_token())
+	fn generate_web_proxy_token(&self, domain: String) -> Result<String, Error> {
+		Ok(self.signer.generate_web_proxy_access_token(domain.into()))
 	}
 
 	fn subscribe_pending(&self, _meta: Self::Metadata, sub: Subscriber<Vec<ConfirmationRequest>>) {
 		self.subscribers.lock().push(sub)
 	}
diff --git a/rpc/src/v1/traits/signer.rs b/rpc/src/v1/traits/signer.rs
index 0280f9612..f71b5f604 100644
--- a/rpc/src/v1/traits/signer.rs
+++ b/rpc/src/v1/traits/signer.rs
@@ -49,13 +49,13 @@ build_rpc_trait! {
 
 		/// Generates new authorization token.
 		#[rpc(name = "signer_generateAuthorizationToken")]
 		fn generate_token(&self) -> Result<String, Error>;
 
-		/// Generates new web proxy access token.
+		/// Generates new web proxy access token for particular domain.
 		#[rpc(name = "signer_generateWebProxyAccessToken")]
-		fn generate_web_proxy_token(&self) -> Result<String, Error>;
+		fn generate_web_proxy_token(&self, String) -> Result<String, Error>;
 
 		#[pubsub(name = "signer_pending")] {
 			/// Subscribe to new pending requests on signer interface.
 			#[rpc(name = "signer_subscribePending")]
 			fn subscribe_pending(&self, Self::Metadata, Subscriber<Vec<ConfirmationRequest>>);

Vulnerable code: use jsonrpc_http_server::{self as http, hyper};
	/// Should return true if token is a valid web proxy access token.
	fn is_web_proxy_token_valid(&self, token: &str) -> bool;
impl<F> WebProxyTokens for F where F: Fn(String) -> bool + Send + Sync {
	fn is_web_proxy_token_valid(&self, token: &str) -> bool { self(token.to_owned()) }
	let (server, fetch) = serve_with_fetch("token");
	let (server, fetch) = serve_with_fetch("token");
	let (server, fetch) = serve_with_fetch("xolrg9fePeQyKLnL");
	let (server, fetch) = serve_with_fetch("token");
	let (server, fetch) = serve_with_fetch("test");
	let (server, fetch) = serve_with_fetch("token");
	let (server, fetch) = serve_with_fetch("token");
	let (server, fetch) = serve_with_fetch("token");
	let (server, fetch) = serve_with_fetch("token");
pub fn serve_with_fetch(web_token: &'static str) -> (Server, FakeFetch) {
			.web_proxy_tokens(Arc::new(move |token| &token == web_token))
			web_proxy_tokens: Arc::new(|_| false),
		match token {
			Some(token) if self.web_proxy_tokens.is_web_proxy_token_valid(token) => {},
		}
  generateWebProxyAccessToken () {
      .execute('signer_generateWebProxyAccessToken');
    params: [],
    transaction(() => {
      let url = (_url || this.nextUrl).trim().replace(/\/+$/, '');
      if (!hasProtocol.test(url)) {
        url = `https://${url}`;
      }
      this.setNextUrl(url);
      this.setCurrentUrl(this.nextUrl);
  generateToken = () => {
      .generateWebProxyAccessToken()
        store.gotoUrl();
        expect(store.currentUrl).to.equal('https://parity.io');
        store.gotoUrl('google.com');

        expect(store.currentUrl).to.equal('https://google.com');
    return this.store.generateToken();
		let web_proxy_tokens = Arc::new(move |token| signer.is_valid_web_proxy_access_token(&token));
	web_proxy_tokens: Mutex<TransientHashMap<String, ()>>,
	pub fn is_valid_web_proxy_access_token(&self, token: &String) -> bool {
		self.web_proxy_tokens.lock().contains_key(&token)
	pub fn generate_web_proxy_access_token(&self) -> String {
		tokens.insert(token.clone(), ());
	fn generate_web_proxy_token(&self) -> Result<String, Error> {
		Ok(self.signer.generate_web_proxy_access_token())
		/// Generates new web proxy access token.
		fn generate_web_proxy_token(&self) -> Result<String, Error>;
Fixed code: use jsonrpc_http_server::{self as http, hyper, Origin};
	/// Should return a domain allowed to be accessed by this token or `None` if the token is not valid
	fn domain(&self, token: &str) -> Option<Origin>;
impl<F> WebProxyTokens for F where F: Fn(String) -> Option<Origin> + Send + Sync {
	fn domain(&self, token: &str) -> Option<Origin> { self(token.to_owned()) }
	let (server, fetch) = serve_with_fetch("token", "https://parity.io");
	let (server, fetch) = serve_with_fetch("token", "https://parity.io");
	let (server, fetch) = serve_with_fetch("xolrg9fePeQyKLnL", "https://contribution.melonport.com");
	let (server, fetch) = serve_with_fetch("token", "https://parity.io");
#[test]
fn should_return_error_on_non_whitelisted_domain() {
	// given
	let (server, fetch) = serve_with_fetch("token", "https://ethcore.io");

	// when
	let response = request(server,
		"\
			GET / HTTP/1.1\r\n\
			Host: EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY.web.web3.site\r\n\
			Connection: close\r\n\
			\r\n\
		"
	);

	// then
	response.assert_status("HTTP/1.1 400 Bad Request");
	assert_security_headers_for_embed(&response.headers);

	fetch.assert_no_more_requests();
}

	let (server, fetch) = serve_with_fetch("test", "https://parity.io");
	let (server, fetch) = serve_with_fetch("token", "ftp://parity.io");
	let (server, fetch) = serve_with_fetch("token", "https://parity.io");
	let (server, fetch) = serve_with_fetch("token", "https://parity.io");
	let (server, fetch) = serve_with_fetch("token", "https://parity.io");
pub fn serve_with_fetch(web_token: &'static str, domain: &'static str) -> (Server, FakeFetch) {
			.web_proxy_tokens(Arc::new(move |token| {
				if &token == web_token { Some(domain.into()) } else { None }
			}))
			web_proxy_tokens: Arc::new(|_| None),
		let domain = match token.and_then(|token| self.web_proxy_tokens.domain(token)) {
			Some(domain) => domain,
		};
		if !target_url.starts_with(&*domain) {
			return Err(State::Error(ContentHandler::error(
				StatusCode::BadRequest, "Invalid Domain", "Dapp attempted to access invalid domain.", Some(&target_url), self.embeddable_on.clone(),
			)));
		}

  generateWebProxyAccessToken (domain) {
      .execute('signer_generateWebProxyAccessToken', domain);
    params: [{
      type: String,
      desc: 'Domain for which the token is valid. Only requests to this domain will be allowed.',
      example: 'https://parity.io'
    }],
    let url = (_url || this.nextUrl).trim().replace(/\/+$/, '');
    if (!hasProtocol.test(url)) {
      url = `https://${url}`;
    }
    return this.generateToken(url).then(() => {
      transaction(() => {
        this.setNextUrl(url);
        this.setCurrentUrl(this.nextUrl);
      });
  generateToken = (_url) => {
      .generateWebProxyAccessToken(_url)
        return store.gotoUrl().then(() => {
          expect(store.currentUrl).to.equal('https://parity.io');
        });
        return store.gotoUrl('google.com').then(() => {
          expect(store.currentUrl).to.equal('https://google.com');
        });
		let web_proxy_tokens = Arc::new(move |token| signer.web_proxy_access_token_domain(&token));
use http::Origin;
	web_proxy_tokens: Mutex<TransientHashMap<String, Origin>>,
	pub fn web_proxy_access_token_domain(&self, token: &String) -> Option<Origin> {
		self.web_proxy_tokens.lock().get(token).cloned()
	pub fn generate_web_proxy_access_token(&self, domain: Origin) -> String {
		tokens.insert(token.clone(), domain);
	fn generate_web_proxy_token(&self, domain: String) -> Result<String, Error> {
		Ok(self.signer.generate_web_proxy_access_token(domain.into()))
		/// Generates new web proxy access token for particular domain.
		fn generate_web_proxy_token(&self, String) -> Result<String, Error>;