From d9ca69439c30b71f9a519a37d9a991bc5fd639fd Mon Sep 17 00:00:00 2001 From: Lauritz Holtmann Date: Sun, 16 Jan 2022 22:11:37 +0100 Subject: [PATCH 1/8] Add redirect_uri attack examples --- popup.js | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/popup.js b/popup.js index f7f3ae9..33a9bfa 100644 --- a/popup.js +++ b/popup.js @@ -239,8 +239,11 @@ function performAnalysis(params) { // Adjust Redirect URI: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.1.3 if(params.get('redirect_uri')) { list_element = document.createElement("li"); - list_element.innerHTML = 'If the \'redirect_uri\' parameter is present, the authorization server MUST compare it against pre-defined redirection URI values using simple string comparison (RFC3986). Try to fiddle around with different schemes, (sub-)domains, paths, query parameters and fragments. Lax validation may lead to token disclosure. See literature.'; + list_element.innerHTML = 'If the \'redirect_uri\' parameter is present, the authorization server MUST compare it against pre-defined redirection URI values using simple string comparison (RFC3986). Try to fiddle around with different schemes, (sub-)domains, paths, query parameters and fragments. Lax validation may lead to token disclosure. Exemplary attack ideas: See literature.'; attacksList.appendChild(list_element); + Array.from(document.getElementsByClassName("attackRedirectUri")).forEach(function(button) { + button.addEventListener("click", launchAttackRedirectUri, false); + }); } } @@ -360,6 +363,42 @@ function launchAttackResponseMode() { setParameterAndReload("response_mode", "fragment"); } +function launchAttackRedirectUri(event) { + let variant = parseInt(event.target.dataset.variant); + let redirect_uri = new URL(urlParams.get("redirect_uri")); + + // Manipulate redirect_uri depending on the clicked button + switch (variant) { + case 0: + // Use http:// scheme (we assume here that the default is https://) + redirect_uri.protocol = "http:"; + break; + case 1: + // Use aura-test:// scheme (should be non-existent) + // Scenario: If this works, a native app could be used to leak the Auth. Response + redirect_uri.protocol = "aura-test:"; + break; + case 2: + // Append something to path + // Scenario: If this works, a XSS or open redirect can be used to leak the Auth. Response + if(redirect_uri.pathname.slice(-1) === "/") { + redirect_uri.pathname = redirect_uri.pathname + "aura-test"; + } + else { + redirect_uri.pathname = redirect_uri.pathname + "/aura-test"; + } + break; + case 3: + // Use imaginary Subdomain + // Scenario: If this works, a XSS or open redirect or subdomain takeover can be used to leak the Auth. Response on any subdomain + redirect_uri.hostname = "aura-test." + redirect_uri.hostname; + break; + default: + alert("Whoops, something went wrong :("); + } + setParameterAndReload("redirect_uri", redirect_uri.toString()); +} + /**************************************************************************************************/ document.addEventListener("DOMContentLoaded", function() { // Event listeners for UI elements From 1f458c997b4670d85a943e729b836be705f75aba Mon Sep 17 00:00:00 2001 From: Lauritz Holtmann Date: Mon, 17 Jan 2022 00:03:18 +0100 Subject: [PATCH 2/8] Refactoring to introduce alternative view via devtools instead of popup --- README.md | 2 +- manifest.json | 3 +- popup.html | 157 ------------------- popup.js | 418 -------------------------------------------------- 4 files changed, 3 insertions(+), 577 deletions(-) delete mode 100644 popup.html delete mode 100644 popup.js diff --git a/README.md b/README.md index 478edcb..e5324b9 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This Chromium extensions aims to support the analysis of single sign-on implemen ![Demo Screenshot](demo_screenshot2.png) ## Features -* View request parameters at a glance. +* View request parameters at a glance, either via the *popup* or the *developer tools panel*. * Hover over standardized parameters for background information about parameters. * Manually modify request parameters. * Detailed Analysis of request parameters: diff --git a/manifest.json b/manifest.json index de2ebdd..45de505 100644 --- a/manifest.json +++ b/manifest.json @@ -3,8 +3,9 @@ "name": "AuRA - Auth. Request Analyser", "version": "1.0.4", "action": { - "default_popup": "popup.html" + "default_popup": "application.html" }, + "devtools_page": "devtools.html", "background": { "service_worker": "background.js" }, diff --git a/popup.html b/popup.html deleted file mode 100644 index e647922..0000000 --- a/popup.html +++ /dev/null @@ -1,157 +0,0 @@ - - - - - - AuRA - Auth. Request Analyser - - - - -

Auth. Request Analyser

- - - - -
ParameterValue
- -
- -
-

Uh oh! Apparently this is not an Auth. Request ☹️

-

To configure and implement a single sign-on flow based on OAuth 2.0 or OpenID Connect 1.0 can be challenging. This extensions supports the analysis of the Auth. Request, as many misconfigurations and resulting weaknesses can be identified in this single request.

-

Unfortunately, this page apparently does not belong to such an Auth. Request. Do you want to search your recent browser history for results that appear to belong to such requests? The output is limited to at most 10 results:

- - - - -
- - - - - - - -
- - - - - - - \ No newline at end of file diff --git a/popup.js b/popup.js deleted file mode 100644 index 33a9bfa..0000000 --- a/popup.js +++ /dev/null @@ -1,418 +0,0 @@ -/** - * (c) Lauritz Holtmann, https://security.lauritz-holtmann.de - */ - -let run = document.getElementById("run"); -let saveUrl = document.getElementById("saveUrl"); -let restoreUrl = document.getElementById("restoreUrl"); -let attacksList = document.getElementById("attacksList"); -let analysisList = document.getElementById("analysisList"); -let noAuthRequest = document.getElementById("noAuthRequest"); -let searchHistory = document.getElementById("searchHistory"); -let parameterForm = document.getElementById("parameterForm"); -let parameterTable = document.getElementById("parameterTable"); -let observationsList = document.getElementById("observationsList"); -let authRequestsForm = document.getElementById("authRequestsForm"); -let analysisContainer = document.getElementById("analysisContainer"); -let parameterFormInput = document.getElementById("parameterFormInput"); -let parameterFormSelect = document.getElementById("parameterFormSelect"); -let parameterFormButton = document.getElementById("parameterFormButton"); -let authRequestsFormButton = document.getElementById("authRequestsFormButton"); -let authRequestsFormSelect = document.getElementById("authRequestsFormSelect"); -let authRequestsFormNoResults = document.getElementById("authRequestsFormNoResults"); - -let url; -let urlParams; - -let knowledgeBase = { - "oauthParams": { - "response_type":{ - "allowed":["code","id_token","code id_token"], - "deprecated":["token","code token","token id_token"], - "description":"This parameter specifies which Grant should be used.", - "required":true - }, - "redirect_uri":{ - "description":"This parameter specifies where the Auth. Response including sensitive secrets should be sent.", - "required":false - }, - "state":{ - "required":false, - "recommended":true, - "description":"This parameter SHOULD be used to prevent CSRF, as it enables the client (= relying party) to maintain a state between Auth. Request and Auth. Response. The parameter MUST be bound to the end-user session." - }, - "client_id":{ - "required":true, - "description":"This parameter identifies the client." - }, - "app_id":{ - "required":false, - "description":"Some Authorization Servers (like Instagram) use this as synonym to the client_id, which normally identifies the client." - }, - "scope":{ - "required":false, - "description":"This parameter defines the requested access scope." - }, - "response_mode":{ - "allowed":["query","fragment"], - "description":"This parameter allows to specify how the Auth. Response parameters are sent." - }, - "prompt":{ - "required":false, - "description":"This parameter specifies whether the Auth. Server should prompt the user for reauthentication or consent." - }, - "request_uri":{ - "required":false, - "description":"This optional OpenID Connect parameter allows to pass the request by reference and is well-known to cause SSRF issues." - } - } -} - -function isAuthRequest(params) { - // 1) According to rfc6749 response_type and client_id are required - // 2) app_id is used in some cases by Instagram instead of client_id - // 3) Facebook requires client_id, redirect_uri and state: https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/?locale=en_US - return ((params.get('response_type') && (params.get('client_id') || params.get('app_id'))) || // app_id is for instance used by Instagram in some cases... - (params.get('client_id') && params.get('redirect_uri') && params.get('state')) // Facebook requires client_id, redirect_uri and state - ); -} - -function processAuthRequest(urlString) { - url = new URL(urlString); - urlParams = new URLSearchParams(url.search); - - if (!isAuthRequest(urlParams)) { - console.log("Error: The given URL does not include all REQUIRED parameters for Auth. Requests."); - return -1; - } else { - noAuthRequest.style.display = "none"; - } - - updateParamTable(urlParams); - createParameterForm(urlParams); - performAnalysis(urlParams); -} - -function updateParamTable(params) { - parameterTable.innerHTML='ParameterValue'; - params.forEach(function(value, key) { - let row = parameterTable.insertRow(-1); - // check if knowledge base includes description for this parameter - if(knowledgeBase["oauthParams"][key] && knowledgeBase["oauthParams"][key]["description"]) { - row.title = knowledgeBase["oauthParams"][key]["description"]; - } - - let parameter = row.insertCell(0); - let val = row.insertCell(1); - - parameter.innerText = key; - val.innerText = value; - }); -} - -function createParameterForm(params) { - parameterForm.removeAttribute("style"); - parameterFormSelect.innerHTML = ""; - parameterFormInput.innerHTML = ""; - - let firstElement = true; - params.forEach(function(value, key) { - let option = document.createElement("option"); - option.text = key; - option.value = key; - parameterFormSelect.add(option); - - let element; - element = document.createElement("input"); - element.value = value; - element.name = key; - element.size = "80"; - element.type = "text"; - - // only display first input field - if(firstElement) { - firstElement = false; - } else { - element.type = "hidden"; - } - - parameterFormInput.appendChild(element); - }); - setFocusRecentParameterLocalStorage(); -} - -function updateParameterForm() { - let selectedString = parameterFormSelect.value; - saveRecentParameterLocalStorage(parameterFormSelect.value); - - document.querySelectorAll('#parameterForm input').forEach(function(inputField) { - if(inputField.name !== selectedString) { - inputField.type = "hidden"; - } else { - inputField.type = "text"; - } - }); -} - -function performAnalysis(params) { - analysisContainer.removeAttribute("style"); - analysisList.innerHTML = ""; - observationsList.innerHTML = ""; - attacksList.innerHTML = ""; - - let list_element; - ////////// Observations - // Scope includes "openid": https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest - if(params.get('scope') && params.get('scope').includes("openid")) { - list_element = document.createElement("li"); - list_element.innerHTML = 'The current flow apparently uses OpenID Connect, as the scope includes \'openid\'. See literature.'; - observationsList.appendChild(list_element); - } - if(params.get('prompt')) { - list_element = document.createElement("li"); - list_element.innerHTML = 'The current flow uses a \'prompt\' parameter. If the Auth. Server supports \'prompt=none\' and follows the specification, the user is not asked for consent before they are sent to the client. See literature.'; - observationsList.appendChild(list_element); - } - - ////////// Checks - // Deprecated grant types: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-2.1.2 - if(params.get('response_type') && params.get('response_type').includes("token")) { - list_element = document.createElement("li"); - list_element.innerHTML = 'Clients SHOULD NOT use response types that include \'token\', because for these flows the authorization server includes the access tokens within the authorization response, which may enable access token leakage and access token replay attacks. See literature.'; - analysisList.appendChild(list_element); - } - - // Check CSRF protection: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.7 - if(!params.get("state") && !params.get("code_challenge") && !params.get("nonce")) { - list_element = document.createElement("li"); - list_element.innerHTML = 'Apparently, no Anti-CSRF measures are used. It is highly recommended to either use a \'state\' value or alternatively use PKCE or the OpenID Connect \'nonce\' value. See literature.'; - analysisList.appendChild(list_element); - } - - // "code_challenge_method" should not be used: https://datatracker.ietf.org/doc/html/rfc7636#section-7.2 - if(params.get("code_challenge_method" === "plain")) { - list_element = document.createElement("li"); - list_element.innerHTML = 'The PKCE extension uses \'code_challenge_method=plain\', which SHOULD NOT be used. See literature.'; - analysisList.appendChild(list_element); - } - // Public Clients no PKCE: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-2.1.1 - if(!params.get('code_challenge')) { - list_element = document.createElement("li"); - list_element.innerHTML = 'The client does not use the Proof Key for Code Exchange (PKCE, RFC7636). If the application is a public client (client credentials can not be stored privately), PKCE MUST be used. Check if the implementation is a public client! See literature.'; - analysisList.appendChild(list_element); - } - - ////////// Attacks - // Implicit Flow supported? https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-2.1.2 - if(params.get('response_type') && !params.get('response_type').includes("token")) { - list_element = document.createElement("li"); - list_element.innerHTML = 'Even though this flow does not use the deprecated implicit grant type, it may be allowed for this client.
Further, you should try if the OIDC hybrid flow is supported.
See literature.'; - attacksList.appendChild(list_element); - document.getElementById("attackImplicitFlowSupported").addEventListener("click", launchAttackImplicitFlowSupported); - document.getElementById("attackHybridFlowSupported").addEventListener("click", launchAttackattackHybridFlowSupported); - } - - // response_mode fragment supported? https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html - if(!params.get('response_mode')) { - list_element = document.createElement("li"); - list_element.innerHTML = 'Even though this flow does not use a \'response_mode\' parameter, you may test if it is supported by the authorization server. In combination with an Open Redirect on an allowed \'redirect_uri\', this may enable token disclosure. . See literature.'; - attacksList.appendChild(list_element); - document.getElementById("attackResponseMode").addEventListener("click", launchAttackResponseMode); - } - - // Change PKCE code_challenge_method to plain: https://datatracker.ietf.org/doc/html/rfc7636#section-7.2 - if(params.get('code_challenge_method') === "S256") { - list_element = document.createElement("li"); - list_element.innerHTML = 'The current flow uses \'S256\' as code_challenge_method, but \'plain\' may also be allowed. The \'plain\' option only exists for compatibility reasons and SHOULD NOT be used´. See literature.'; - attacksList.appendChild(list_element); - document.getElementById("attackPkcePlain").addEventListener("click", launchAttackPkcePlain); - } - - // Add Request URI: https://publ.sec.uni-stuttgart.de/fettkuestersschmitz-csf-2017.pdf - if(!params.get('request_uri')) { - list_element = document.createElement("li"); - list_element.innerHTML = 'The \'request_uri\' parameter is well-known to allow Server-Side Request Forgery by design. Add a request_uri parameter:
See literature.'; - attacksList.appendChild(list_element); - document.getElementById("attackRequestUri").addEventListener("click", launchAttackRequestUri); - } - - // Adjust Redirect URI: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.1.3 - if(params.get('redirect_uri')) { - list_element = document.createElement("li"); - list_element.innerHTML = 'If the \'redirect_uri\' parameter is present, the authorization server MUST compare it against pre-defined redirection URI values using simple string comparison (RFC3986). Try to fiddle around with different schemes, (sub-)domains, paths, query parameters and fragments. Lax validation may lead to token disclosure. Exemplary attack ideas: See literature.'; - attacksList.appendChild(list_element); - Array.from(document.getElementsByClassName("attackRedirectUri")).forEach(function(button) { - button.addEventListener("click", launchAttackRedirectUri, false); - }); - } -} - -function reloadPageWithModifications() { - let selectedString = parameterFormSelect.value; - let selectedStringSanitized = selectedString.replaceAll("'", "\\'"); // we need to escape single quotes - let newValue = document.querySelectorAll(`#parameterForm input[name='${selectedStringSanitized}']`)[0].value; - - setParameterAndReload(selectedString, newValue); -} - -async function runAnalysis() { - chrome.tabs.query({currentWindow: true, active: true}, function(tabs){ - let currentUrl = tabs[0].url; - processAuthRequest(currentUrl); - }) -} - -function saveUrlLocalStorage() { - return chrome.storage.local.set({"savedUrl": url.toString()}); -} - -function restoreUrlLocalStorage() { - chrome.storage.local.get("savedUrl", function(result) { - openPage(result.savedUrl); - }); -} - -function replayHistoryAuthRequest() { - openPage(authRequestsFormSelect.value); -} - -function saveRecentParameterLocalStorage(parameter) { - return chrome.storage.local.set({"recentParameter": parameter}); -} - -function setFocusRecentParameterLocalStorage() { - chrome.storage.local.get("recentParameter", function(result) { - if(result.recentParameter) { - // only set value of select if the option exists - let recentParameterSanitized = result.recentParameter.replaceAll("'", "\\'"); // we need to escape single quotes - if(document.querySelectorAll(`#parameterFormSelect option[value='${recentParameterSanitized}']`).length === 1) { - parameterFormSelect.value = result.recentParameter; - updateParameterForm() - } - } - }); -} - -function setParameterAndReload(parameterName, parameterValue) { - urlParams.set(parameterName, parameterValue); - url.search = urlParams.toString(); - - openPage(url.toString()); -} - -function searchHistoryAuthRequest() { - authRequestsFormSelect.innerHTML = ""; - authRequestsForm.removeAttribute("style"); - - chrome.history.search({ text:"client_id", maxResults:10000 }, function(data) { - let mapTupleSet = [] - data.some(function(page) { - // check if history item matches our heuristics for auth requests and to - // include each client at an IdP only once, we use the client_id to match - let testUrl = new URL(page.url); - let testUrlParams = new URLSearchParams(testUrl.search); - - let mapTuple = {"idp": testUrl.hostname, "client_id" : testUrlParams.get('client_id')}; - if(isAuthRequest(testUrlParams) && - !mapTupleSet.some(tuple => (tuple.idp == mapTuple["idp"] && tuple.client_id == mapTuple["client_id"]))) { - - // add our new mapTuple to the list of tuples - mapTupleSet.push(mapTuple); - - - let option = document.createElement("option"); - // the client_id is often not human readable, thus we print the redirect_uri instead - option.text = `${testUrl.hostname}: ${testUrlParams.get('redirect_uri')}`; - option.value = page.url; - authRequestsFormSelect.add(option); - - // limit the maximal count of entries - return authRequestsFormSelect.length === 10; - } - }); - if(!authRequestsFormSelect.innerHTML) { - authRequestsFormNoResults.removeAttribute("style"); - } - }); -} - -function openPage(url) { - chrome.tabs.query({currentWindow: true, active: true}, function(tabs){ - chrome.tabs.update(tabs[0].id, { url: url }); - }); -} - -////////// Attacks -function launchAttackImplicitFlowSupported() { - setParameterAndReload("response_type", "token"); -} - -function launchAttackattackHybridFlowSupported() { - setParameterAndReload("response_type", "code token"); -} - -function launchAttackPkcePlain() { - setParameterAndReload("code_challenge_method", "plain"); -} - -function launchAttackRequestUri() { - setParameterAndReload("request_uri", document.getElementById("attackRequestUriValue").value); -} - -function launchAttackResponseMode() { - setParameterAndReload("response_mode", "fragment"); -} - -function launchAttackRedirectUri(event) { - let variant = parseInt(event.target.dataset.variant); - let redirect_uri = new URL(urlParams.get("redirect_uri")); - - // Manipulate redirect_uri depending on the clicked button - switch (variant) { - case 0: - // Use http:// scheme (we assume here that the default is https://) - redirect_uri.protocol = "http:"; - break; - case 1: - // Use aura-test:// scheme (should be non-existent) - // Scenario: If this works, a native app could be used to leak the Auth. Response - redirect_uri.protocol = "aura-test:"; - break; - case 2: - // Append something to path - // Scenario: If this works, a XSS or open redirect can be used to leak the Auth. Response - if(redirect_uri.pathname.slice(-1) === "/") { - redirect_uri.pathname = redirect_uri.pathname + "aura-test"; - } - else { - redirect_uri.pathname = redirect_uri.pathname + "/aura-test"; - } - break; - case 3: - // Use imaginary Subdomain - // Scenario: If this works, a XSS or open redirect or subdomain takeover can be used to leak the Auth. Response on any subdomain - redirect_uri.hostname = "aura-test." + redirect_uri.hostname; - break; - default: - alert("Whoops, something went wrong :("); - } - setParameterAndReload("redirect_uri", redirect_uri.toString()); -} - -/**************************************************************************************************/ -document.addEventListener("DOMContentLoaded", function() { - // Event listeners for UI elements - parameterFormSelect.addEventListener("change", updateParameterForm); - parameterFormButton.addEventListener("click", reloadPageWithModifications); - run.addEventListener("click", runAnalysis); - saveUrl.addEventListener("click", saveUrlLocalStorage); - restoreUrl.addEventListener("click", restoreUrlLocalStorage); - searchHistory.addEventListener("click", searchHistoryAuthRequest); - authRequestsFormButton.addEventListener("click", replayHistoryAuthRequest); - - // make sure to trigger analysis if popup is opened during page browse - chrome.tabs.onUpdated.addListener(runAnalysis); - - // Initial analysis on popup open - runAnalysis(); -}); \ No newline at end of file From 61f4552a3d390793060506d955ca82fbc2c7644a Mon Sep 17 00:00:00 2001 From: Lauritz Holtmann Date: Mon, 17 Jan 2022 00:16:33 +0100 Subject: [PATCH 3/8] Refactoring to introduce alternative view via devtools instead of popup --- application.html | 157 ++++++++++++++++++ application.js | 418 +++++++++++++++++++++++++++++++++++++++++++++++ devtools.html | 1 + devtools.js | 5 + 4 files changed, 581 insertions(+) create mode 100644 application.html create mode 100644 application.js create mode 100644 devtools.html create mode 100644 devtools.js diff --git a/application.html b/application.html new file mode 100644 index 0000000..8542841 --- /dev/null +++ b/application.html @@ -0,0 +1,157 @@ + + + + + + AuRA - Auth. Request Analyser + + + + +

Auth. Request Analyser

+ + + + +
ParameterValue
+ +
+ +
+

Uh oh! Apparently this is not an Auth. Request ☹️

+

To configure and implement a single sign-on flow based on OAuth 2.0 or OpenID Connect 1.0 can be challenging. This extensions supports the analysis of the Auth. Request, as many misconfigurations and resulting weaknesses can be identified in this single request.

+

Unfortunately, this page apparently does not belong to such an Auth. Request. Do you want to search your recent browser history for results that appear to belong to such requests? The output is limited to at most 10 results:

+ + + + +
+ + + + + + + +
+ + + + + + + \ No newline at end of file diff --git a/application.js b/application.js new file mode 100644 index 0000000..33a9bfa --- /dev/null +++ b/application.js @@ -0,0 +1,418 @@ +/** + * (c) Lauritz Holtmann, https://security.lauritz-holtmann.de + */ + +let run = document.getElementById("run"); +let saveUrl = document.getElementById("saveUrl"); +let restoreUrl = document.getElementById("restoreUrl"); +let attacksList = document.getElementById("attacksList"); +let analysisList = document.getElementById("analysisList"); +let noAuthRequest = document.getElementById("noAuthRequest"); +let searchHistory = document.getElementById("searchHistory"); +let parameterForm = document.getElementById("parameterForm"); +let parameterTable = document.getElementById("parameterTable"); +let observationsList = document.getElementById("observationsList"); +let authRequestsForm = document.getElementById("authRequestsForm"); +let analysisContainer = document.getElementById("analysisContainer"); +let parameterFormInput = document.getElementById("parameterFormInput"); +let parameterFormSelect = document.getElementById("parameterFormSelect"); +let parameterFormButton = document.getElementById("parameterFormButton"); +let authRequestsFormButton = document.getElementById("authRequestsFormButton"); +let authRequestsFormSelect = document.getElementById("authRequestsFormSelect"); +let authRequestsFormNoResults = document.getElementById("authRequestsFormNoResults"); + +let url; +let urlParams; + +let knowledgeBase = { + "oauthParams": { + "response_type":{ + "allowed":["code","id_token","code id_token"], + "deprecated":["token","code token","token id_token"], + "description":"This parameter specifies which Grant should be used.", + "required":true + }, + "redirect_uri":{ + "description":"This parameter specifies where the Auth. Response including sensitive secrets should be sent.", + "required":false + }, + "state":{ + "required":false, + "recommended":true, + "description":"This parameter SHOULD be used to prevent CSRF, as it enables the client (= relying party) to maintain a state between Auth. Request and Auth. Response. The parameter MUST be bound to the end-user session." + }, + "client_id":{ + "required":true, + "description":"This parameter identifies the client." + }, + "app_id":{ + "required":false, + "description":"Some Authorization Servers (like Instagram) use this as synonym to the client_id, which normally identifies the client." + }, + "scope":{ + "required":false, + "description":"This parameter defines the requested access scope." + }, + "response_mode":{ + "allowed":["query","fragment"], + "description":"This parameter allows to specify how the Auth. Response parameters are sent." + }, + "prompt":{ + "required":false, + "description":"This parameter specifies whether the Auth. Server should prompt the user for reauthentication or consent." + }, + "request_uri":{ + "required":false, + "description":"This optional OpenID Connect parameter allows to pass the request by reference and is well-known to cause SSRF issues." + } + } +} + +function isAuthRequest(params) { + // 1) According to rfc6749 response_type and client_id are required + // 2) app_id is used in some cases by Instagram instead of client_id + // 3) Facebook requires client_id, redirect_uri and state: https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/?locale=en_US + return ((params.get('response_type') && (params.get('client_id') || params.get('app_id'))) || // app_id is for instance used by Instagram in some cases... + (params.get('client_id') && params.get('redirect_uri') && params.get('state')) // Facebook requires client_id, redirect_uri and state + ); +} + +function processAuthRequest(urlString) { + url = new URL(urlString); + urlParams = new URLSearchParams(url.search); + + if (!isAuthRequest(urlParams)) { + console.log("Error: The given URL does not include all REQUIRED parameters for Auth. Requests."); + return -1; + } else { + noAuthRequest.style.display = "none"; + } + + updateParamTable(urlParams); + createParameterForm(urlParams); + performAnalysis(urlParams); +} + +function updateParamTable(params) { + parameterTable.innerHTML='ParameterValue'; + params.forEach(function(value, key) { + let row = parameterTable.insertRow(-1); + // check if knowledge base includes description for this parameter + if(knowledgeBase["oauthParams"][key] && knowledgeBase["oauthParams"][key]["description"]) { + row.title = knowledgeBase["oauthParams"][key]["description"]; + } + + let parameter = row.insertCell(0); + let val = row.insertCell(1); + + parameter.innerText = key; + val.innerText = value; + }); +} + +function createParameterForm(params) { + parameterForm.removeAttribute("style"); + parameterFormSelect.innerHTML = ""; + parameterFormInput.innerHTML = ""; + + let firstElement = true; + params.forEach(function(value, key) { + let option = document.createElement("option"); + option.text = key; + option.value = key; + parameterFormSelect.add(option); + + let element; + element = document.createElement("input"); + element.value = value; + element.name = key; + element.size = "80"; + element.type = "text"; + + // only display first input field + if(firstElement) { + firstElement = false; + } else { + element.type = "hidden"; + } + + parameterFormInput.appendChild(element); + }); + setFocusRecentParameterLocalStorage(); +} + +function updateParameterForm() { + let selectedString = parameterFormSelect.value; + saveRecentParameterLocalStorage(parameterFormSelect.value); + + document.querySelectorAll('#parameterForm input').forEach(function(inputField) { + if(inputField.name !== selectedString) { + inputField.type = "hidden"; + } else { + inputField.type = "text"; + } + }); +} + +function performAnalysis(params) { + analysisContainer.removeAttribute("style"); + analysisList.innerHTML = ""; + observationsList.innerHTML = ""; + attacksList.innerHTML = ""; + + let list_element; + ////////// Observations + // Scope includes "openid": https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest + if(params.get('scope') && params.get('scope').includes("openid")) { + list_element = document.createElement("li"); + list_element.innerHTML = 'The current flow apparently uses OpenID Connect, as the scope includes \'openid\'. See literature.'; + observationsList.appendChild(list_element); + } + if(params.get('prompt')) { + list_element = document.createElement("li"); + list_element.innerHTML = 'The current flow uses a \'prompt\' parameter. If the Auth. Server supports \'prompt=none\' and follows the specification, the user is not asked for consent before they are sent to the client. See literature.'; + observationsList.appendChild(list_element); + } + + ////////// Checks + // Deprecated grant types: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-2.1.2 + if(params.get('response_type') && params.get('response_type').includes("token")) { + list_element = document.createElement("li"); + list_element.innerHTML = 'Clients SHOULD NOT use response types that include \'token\', because for these flows the authorization server includes the access tokens within the authorization response, which may enable access token leakage and access token replay attacks. See literature.'; + analysisList.appendChild(list_element); + } + + // Check CSRF protection: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.7 + if(!params.get("state") && !params.get("code_challenge") && !params.get("nonce")) { + list_element = document.createElement("li"); + list_element.innerHTML = 'Apparently, no Anti-CSRF measures are used. It is highly recommended to either use a \'state\' value or alternatively use PKCE or the OpenID Connect \'nonce\' value. See literature.'; + analysisList.appendChild(list_element); + } + + // "code_challenge_method" should not be used: https://datatracker.ietf.org/doc/html/rfc7636#section-7.2 + if(params.get("code_challenge_method" === "plain")) { + list_element = document.createElement("li"); + list_element.innerHTML = 'The PKCE extension uses \'code_challenge_method=plain\', which SHOULD NOT be used. See literature.'; + analysisList.appendChild(list_element); + } + // Public Clients no PKCE: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-2.1.1 + if(!params.get('code_challenge')) { + list_element = document.createElement("li"); + list_element.innerHTML = 'The client does not use the Proof Key for Code Exchange (PKCE, RFC7636). If the application is a public client (client credentials can not be stored privately), PKCE MUST be used. Check if the implementation is a public client! See literature.'; + analysisList.appendChild(list_element); + } + + ////////// Attacks + // Implicit Flow supported? https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-2.1.2 + if(params.get('response_type') && !params.get('response_type').includes("token")) { + list_element = document.createElement("li"); + list_element.innerHTML = 'Even though this flow does not use the deprecated implicit grant type, it may be allowed for this client.
Further, you should try if the OIDC hybrid flow is supported.
See literature.'; + attacksList.appendChild(list_element); + document.getElementById("attackImplicitFlowSupported").addEventListener("click", launchAttackImplicitFlowSupported); + document.getElementById("attackHybridFlowSupported").addEventListener("click", launchAttackattackHybridFlowSupported); + } + + // response_mode fragment supported? https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html + if(!params.get('response_mode')) { + list_element = document.createElement("li"); + list_element.innerHTML = 'Even though this flow does not use a \'response_mode\' parameter, you may test if it is supported by the authorization server. In combination with an Open Redirect on an allowed \'redirect_uri\', this may enable token disclosure. . See literature.'; + attacksList.appendChild(list_element); + document.getElementById("attackResponseMode").addEventListener("click", launchAttackResponseMode); + } + + // Change PKCE code_challenge_method to plain: https://datatracker.ietf.org/doc/html/rfc7636#section-7.2 + if(params.get('code_challenge_method') === "S256") { + list_element = document.createElement("li"); + list_element.innerHTML = 'The current flow uses \'S256\' as code_challenge_method, but \'plain\' may also be allowed. The \'plain\' option only exists for compatibility reasons and SHOULD NOT be used´. See literature.'; + attacksList.appendChild(list_element); + document.getElementById("attackPkcePlain").addEventListener("click", launchAttackPkcePlain); + } + + // Add Request URI: https://publ.sec.uni-stuttgart.de/fettkuestersschmitz-csf-2017.pdf + if(!params.get('request_uri')) { + list_element = document.createElement("li"); + list_element.innerHTML = 'The \'request_uri\' parameter is well-known to allow Server-Side Request Forgery by design. Add a request_uri parameter:
See literature.'; + attacksList.appendChild(list_element); + document.getElementById("attackRequestUri").addEventListener("click", launchAttackRequestUri); + } + + // Adjust Redirect URI: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.1.3 + if(params.get('redirect_uri')) { + list_element = document.createElement("li"); + list_element.innerHTML = 'If the \'redirect_uri\' parameter is present, the authorization server MUST compare it against pre-defined redirection URI values using simple string comparison (RFC3986). Try to fiddle around with different schemes, (sub-)domains, paths, query parameters and fragments. Lax validation may lead to token disclosure. Exemplary attack ideas: See literature.'; + attacksList.appendChild(list_element); + Array.from(document.getElementsByClassName("attackRedirectUri")).forEach(function(button) { + button.addEventListener("click", launchAttackRedirectUri, false); + }); + } +} + +function reloadPageWithModifications() { + let selectedString = parameterFormSelect.value; + let selectedStringSanitized = selectedString.replaceAll("'", "\\'"); // we need to escape single quotes + let newValue = document.querySelectorAll(`#parameterForm input[name='${selectedStringSanitized}']`)[0].value; + + setParameterAndReload(selectedString, newValue); +} + +async function runAnalysis() { + chrome.tabs.query({currentWindow: true, active: true}, function(tabs){ + let currentUrl = tabs[0].url; + processAuthRequest(currentUrl); + }) +} + +function saveUrlLocalStorage() { + return chrome.storage.local.set({"savedUrl": url.toString()}); +} + +function restoreUrlLocalStorage() { + chrome.storage.local.get("savedUrl", function(result) { + openPage(result.savedUrl); + }); +} + +function replayHistoryAuthRequest() { + openPage(authRequestsFormSelect.value); +} + +function saveRecentParameterLocalStorage(parameter) { + return chrome.storage.local.set({"recentParameter": parameter}); +} + +function setFocusRecentParameterLocalStorage() { + chrome.storage.local.get("recentParameter", function(result) { + if(result.recentParameter) { + // only set value of select if the option exists + let recentParameterSanitized = result.recentParameter.replaceAll("'", "\\'"); // we need to escape single quotes + if(document.querySelectorAll(`#parameterFormSelect option[value='${recentParameterSanitized}']`).length === 1) { + parameterFormSelect.value = result.recentParameter; + updateParameterForm() + } + } + }); +} + +function setParameterAndReload(parameterName, parameterValue) { + urlParams.set(parameterName, parameterValue); + url.search = urlParams.toString(); + + openPage(url.toString()); +} + +function searchHistoryAuthRequest() { + authRequestsFormSelect.innerHTML = ""; + authRequestsForm.removeAttribute("style"); + + chrome.history.search({ text:"client_id", maxResults:10000 }, function(data) { + let mapTupleSet = [] + data.some(function(page) { + // check if history item matches our heuristics for auth requests and to + // include each client at an IdP only once, we use the client_id to match + let testUrl = new URL(page.url); + let testUrlParams = new URLSearchParams(testUrl.search); + + let mapTuple = {"idp": testUrl.hostname, "client_id" : testUrlParams.get('client_id')}; + if(isAuthRequest(testUrlParams) && + !mapTupleSet.some(tuple => (tuple.idp == mapTuple["idp"] && tuple.client_id == mapTuple["client_id"]))) { + + // add our new mapTuple to the list of tuples + mapTupleSet.push(mapTuple); + + + let option = document.createElement("option"); + // the client_id is often not human readable, thus we print the redirect_uri instead + option.text = `${testUrl.hostname}: ${testUrlParams.get('redirect_uri')}`; + option.value = page.url; + authRequestsFormSelect.add(option); + + // limit the maximal count of entries + return authRequestsFormSelect.length === 10; + } + }); + if(!authRequestsFormSelect.innerHTML) { + authRequestsFormNoResults.removeAttribute("style"); + } + }); +} + +function openPage(url) { + chrome.tabs.query({currentWindow: true, active: true}, function(tabs){ + chrome.tabs.update(tabs[0].id, { url: url }); + }); +} + +////////// Attacks +function launchAttackImplicitFlowSupported() { + setParameterAndReload("response_type", "token"); +} + +function launchAttackattackHybridFlowSupported() { + setParameterAndReload("response_type", "code token"); +} + +function launchAttackPkcePlain() { + setParameterAndReload("code_challenge_method", "plain"); +} + +function launchAttackRequestUri() { + setParameterAndReload("request_uri", document.getElementById("attackRequestUriValue").value); +} + +function launchAttackResponseMode() { + setParameterAndReload("response_mode", "fragment"); +} + +function launchAttackRedirectUri(event) { + let variant = parseInt(event.target.dataset.variant); + let redirect_uri = new URL(urlParams.get("redirect_uri")); + + // Manipulate redirect_uri depending on the clicked button + switch (variant) { + case 0: + // Use http:// scheme (we assume here that the default is https://) + redirect_uri.protocol = "http:"; + break; + case 1: + // Use aura-test:// scheme (should be non-existent) + // Scenario: If this works, a native app could be used to leak the Auth. Response + redirect_uri.protocol = "aura-test:"; + break; + case 2: + // Append something to path + // Scenario: If this works, a XSS or open redirect can be used to leak the Auth. Response + if(redirect_uri.pathname.slice(-1) === "/") { + redirect_uri.pathname = redirect_uri.pathname + "aura-test"; + } + else { + redirect_uri.pathname = redirect_uri.pathname + "/aura-test"; + } + break; + case 3: + // Use imaginary Subdomain + // Scenario: If this works, a XSS or open redirect or subdomain takeover can be used to leak the Auth. Response on any subdomain + redirect_uri.hostname = "aura-test." + redirect_uri.hostname; + break; + default: + alert("Whoops, something went wrong :("); + } + setParameterAndReload("redirect_uri", redirect_uri.toString()); +} + +/**************************************************************************************************/ +document.addEventListener("DOMContentLoaded", function() { + // Event listeners for UI elements + parameterFormSelect.addEventListener("change", updateParameterForm); + parameterFormButton.addEventListener("click", reloadPageWithModifications); + run.addEventListener("click", runAnalysis); + saveUrl.addEventListener("click", saveUrlLocalStorage); + restoreUrl.addEventListener("click", restoreUrlLocalStorage); + searchHistory.addEventListener("click", searchHistoryAuthRequest); + authRequestsFormButton.addEventListener("click", replayHistoryAuthRequest); + + // make sure to trigger analysis if popup is opened during page browse + chrome.tabs.onUpdated.addListener(runAnalysis); + + // Initial analysis on popup open + runAnalysis(); +}); \ No newline at end of file diff --git a/devtools.html b/devtools.html new file mode 100644 index 0000000..36b7bef --- /dev/null +++ b/devtools.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/devtools.js b/devtools.js new file mode 100644 index 0000000..0d31bb8 --- /dev/null +++ b/devtools.js @@ -0,0 +1,5 @@ +chrome.devtools.panels.create("AuRA", + "aura_logo_favicon.png", + "application.html", + function(panel) {} +); \ No newline at end of file From 6b41cb08183273c4a7fc120ad846e4b718a60897 Mon Sep 17 00:00:00 2001 From: Lauritz Holtmann Date: Mon, 17 Jan 2022 23:46:36 +0100 Subject: [PATCH 4/8] Add build script and document updated build process --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e5324b9..daeeb36 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,13 @@ This Chromium extensions aims to support the analysis of single sign-on implemen It is highly recommended to use the latest stable release from [Chrome WebStore](https://chrome.google.com/webstore/detail/clonpaankbndgnciijbiokgjeofjdpeg). -Alternatively, you may either use the latest build published in this repository or directly use the *unpacked sources*. To use the *unpacked sources*, follow these steps: +Alternatively, you may either use the latest build published in this repository or directly use the *unpacked sources*. To use the *unpacked sources*, follow these steps (macOS, Linux): 1. Clone this repository. -2. Visit chrome://extensions/. -3. Enable *Developer mode* (attention, do not enable this option in your "productive" browser!). -4. Specify the cloned folder. +2. Execute `build.sh` script. +3. Unpack created ZIP archive (`auth-request-analyser_submission_chrome_yy-mm-dd-HH-MM-SS.zip`). +4. Visit chrome://extensions/. +5. Enable *Developer mode* (attention, do not enable this option in your "productive" browser!). +6. Specify the cloned folder. ## Privacy From 61eb036ead3ddf647e80b938e3c72488f945164e Mon Sep 17 00:00:00 2001 From: Lauritz Holtmann Date: Mon, 17 Jan 2022 23:47:01 +0100 Subject: [PATCH 5/8] Add build script and document updated build process --- build.sh | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100755 build.sh diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..6f5fbac --- /dev/null +++ b/build.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +# Helper Methods +copy_sources_to_tmp_generic () { + cp -r . /tmp/auth-request-analyser-generic +} + +copy_sources_tmp_chrome () { + cp -r /tmp/auth-request-analyser-generic /tmp/auth-request-analyser-chrome +} + +remove_sources_tmp () { + rm -rf /tmp/auth-request-analyser-generic + rm -rf /tmp/auth-request-analyser-chrome +} + +cleanup_generic_directory () { + echo " [+] Clean source directory... remove screenshot files" + rm /tmp/auth-request-analyser-generic/*screenshot*.png + echo " [+] Clean source directory... remove .git* files and directories" + rm -rf /tmp/auth-request-analyser-generic/.git* + echo " [+] Clean source directory... remove *.md files" + rm /tmp/auth-request-analyser-generic/*.md + echo " [+] Clean source directory... remove .DS_Store" + rm /tmp/auth-request-analyser-generic/.DS_Store* + echo " [+] Clean source directory... remove build script" + rm /tmp/auth-request-analyser-generic/build.sh +} + +pack_extension_chrome () { + zip -r -j "../auth-request-analyser_submission_chrome_$(date '+%Y-%m-%d-%H-%M-%S').zip" /tmp/auth-request-analyser-chrome/ +} + +create_crx_chrome () { + echo " [+] Extension Key path: $EXTENSION_KEY" + echo " [+] Opening headless chrome and pack extension..." + /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --pack-extension=/tmp/auth-request-analyser-chrome --pack-extension-key=$EXTENSION_KEY + cp /tmp/auth-request-analyser-chrome.crx "../auth-request-analyser-$(date '+%Y-%m-%d-%H-%M-%S').crx" +} + +# Main +main () { + echo "[*] Starting the build process..." +############### Generic Setup + echo "[*] Stage 0: Generic Base Etension" + echo " [+] Copy sources to /tmp" + copy_sources_to_tmp_generic + echo " [+] Clean source directory..." + cleanup_generic_directory + echo "[*] Stage 0: Done." +############### Chrome + echo "[*] Stage 1: Chrome Etension" + echo " [+] Copy sources to Chrome folder" + copy_sources_tmp_chrome + echo " [+] Create ZIP archive" + pack_extension_chrome + #echo " [+] Create .crx bundle" + #create_crx_chrome + echo "[*] Stage 1: Done." +############### Generic Cleanup + echo "[*] Stage 3: Cleanup" + echo " [+] Remove temporary directories from /tmp" + remove_sources_tmp + echo "[*] Stage 3: Done." + echo "[>] All done." +} + +main \ No newline at end of file From 9fa1ed34a07e9573784047e01027087fb134a274 Mon Sep 17 00:00:00 2001 From: Lauritz Holtmann Date: Sat, 22 Jan 2022 11:18:24 +0100 Subject: [PATCH 6/8] Add hint for beta status of developer tools panel --- devtools.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools.js b/devtools.js index 0d31bb8..70a9587 100644 --- a/devtools.js +++ b/devtools.js @@ -1,4 +1,4 @@ -chrome.devtools.panels.create("AuRA", +chrome.devtools.panels.create("AuRA (BETA)", "aura_logo_favicon.png", "application.html", function(panel) {} From 590c69a6364b49332b13d3349e9edc06d08a24ac Mon Sep 17 00:00:00 2001 From: Lauritz Holtmann Date: Sat, 22 Jan 2022 12:37:50 +0100 Subject: [PATCH 7/8] Add redirect_uri attack variants: parameter pollution --- application.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/application.js b/application.js index 33a9bfa..5b3d27b 100644 --- a/application.js +++ b/application.js @@ -82,7 +82,7 @@ function processAuthRequest(urlString) { urlParams = new URLSearchParams(url.search); if (!isAuthRequest(urlParams)) { - console.log("Error: The given URL does not include all REQUIRED parameters for Auth. Requests."); + console.log("Error: The given URL does not include all REQUIRED parameters for Auth. Requests. It known that some implementations does not follow the spec and only use client_id or app_id. Thus, there may be a change of the detection rules in the future"); return -1; } else { noAuthRequest.style.display = "none"; @@ -223,7 +223,7 @@ function performAnalysis(params) { // Change PKCE code_challenge_method to plain: https://datatracker.ietf.org/doc/html/rfc7636#section-7.2 if(params.get('code_challenge_method') === "S256") { list_element = document.createElement("li"); - list_element.innerHTML = 'The current flow uses \'S256\' as code_challenge_method, but \'plain\' may also be allowed. The \'plain\' option only exists for compatibility reasons and SHOULD NOT be used´. See literature.'; + list_element.innerHTML = 'The current flow uses \'S256\' as code_challenge_method, but \'plain\' may also be allowed. The \'plain\' option only exists for compatibility reasons and SHOULD NOT be used. See literature.'; attacksList.appendChild(list_element); document.getElementById("attackPkcePlain").addEventListener("click", launchAttackPkcePlain); } @@ -239,7 +239,7 @@ function performAnalysis(params) { // Adjust Redirect URI: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.1.3 if(params.get('redirect_uri')) { list_element = document.createElement("li"); - list_element.innerHTML = 'If the \'redirect_uri\' parameter is present, the authorization server MUST compare it against pre-defined redirection URI values using simple string comparison (RFC3986). Try to fiddle around with different schemes, (sub-)domains, paths, query parameters and fragments. Lax validation may lead to token disclosure. Exemplary attack ideas: See literature.'; + list_element.innerHTML = 'If the \'redirect_uri\' parameter is present, the authorization server MUST compare it against pre-defined redirection URI values using simple string comparison (RFC3986). Try to fiddle around with different schemes, (sub-)domains, paths, query parameters and fragments. Lax validation may lead to token disclosure. Exemplary attack ideas: See literature.'; attacksList.appendChild(list_element); Array.from(document.getElementsByClassName("attackRedirectUri")).forEach(function(button) { button.addEventListener("click", launchAttackRedirectUri, false); @@ -393,6 +393,19 @@ function launchAttackRedirectUri(event) { // Scenario: If this works, a XSS or open redirect or subdomain takeover can be used to leak the Auth. Response on any subdomain redirect_uri.hostname = "aura-test." + redirect_uri.hostname; break; + case 4: + // Add arbitrary parameter + // Scenario: If this works, 1) this may enable open redirect or XSS issues, 2) this may allow parameter pollution: https://security.lauritz-holtmann.de/post/sso-security-redirect-uri-ii/ + redirect_uri.searchParams.set("aura-test", 1); + break; + case 5: + // Add arbitrary location hash - variant of 4 + if(redirect_uri.hash) { + redirect_uri.hash = redirect_uri.hash + "&aura-test=1"; + } else { + redirect_uri.hash = "aura-test"; + } + break; default: alert("Whoops, something went wrong :("); } From ac2a1907555c45b39b8d35c9765f11a10362ae5b Mon Sep 17 00:00:00 2001 From: Lauritz Holtmann Date: Tue, 29 Mar 2022 17:34:18 +0200 Subject: [PATCH 8/8] Bump version --- manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manifest.json b/manifest.json index 45de505..85ce8d1 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "AuRA - Auth. Request Analyser", - "version": "1.0.4", + "version": "1.1", "action": { "default_popup": "application.html" }, @@ -16,4 +16,4 @@ "icons" : { "128":"aura_logo_favicon.png" } -} \ No newline at end of file +}