From fda3ae8ca0dfd6f9b5aa61a09a894408c142d0c4 Mon Sep 17 00:00:00 2001 From: Lukas Zilka Date: Fri, 6 Dec 2013 14:52:22 -0500 Subject: [PATCH 1/5] Integration of security linting after discussion with Hoa. --- _locales/en/messages.json | 30 +- js/chromium/lint_overlay.js | 534 ++++++++++++++++++++++++++++++++++++ js/items_list.js | 24 ++ main.html | 66 +++++ manifest.json | 6 +- 5 files changed, 657 insertions(+), 3 deletions(-) create mode 100644 js/chromium/lint_overlay.js diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d420671..9a1f2ef 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -199,6 +199,10 @@ "message": "Pack", "description": "Text for the link to pack the extension / app." }, + "extensionSettingsLint": { + "message": "Run with Security Lint", + "description": "The link for running the extension lint-er." + }, "packExtensionOverlay": { "message": "Pack Extension", "description": "Title of pack extension dialog." @@ -278,5 +282,29 @@ "managedProfileDialogDescription": { "message": "Applications and extensions cannot be modified by supervised users. Chrome Apps Developer Tools will be closed.", "description": "Content of the dialog for managed profile. It's informing a supervised user that extensions cannot be changed." + }, + "lintStart": { + "message": "Please interact with the app or extension to exercise its full functionality. In the meanwhile the list of redundant permissions and used permissions will be updated.", + "description": "Content of the dialog displayed when the user runs an item with security lint." + }, + "lintStartTitle": { + "message": "Security Lint is now running", + "description": "Title of the dialog displayed when the user runs an item with security lint." + }, + "lintShowManifest": { + "message": "Stop and show modified manifest", + "description": "Text of the button in the linting dialog that triggers ending of linting." + }, + "lintRedundantPermissions": { + "message": "Redundant permissions", + "description": "Title of a list with redundant permissions." + }, + "lintUsedPermissions": { + "message": "Used permissions", + "description": "Title of a list with used permissions." + }, + "lintQuestions": { + "message": "Clarify", + "description": "Title of a list with questions." } -} \ No newline at end of file +} diff --git a/js/chromium/lint_overlay.js b/js/chromium/lint_overlay.js new file mode 100644 index 0000000..831b0a2 --- /dev/null +++ b/js/chromium/lint_overlay.js @@ -0,0 +1,534 @@ +// Copyright (c) 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('lintOverlay', function() { + // List of permissions the user's item requested. + var requestedPermissions = null; + + // Dictionary of permissions that were observed by monitoring the extension. + var exercisedPermissions = null; + + // Dictionary for keeping track of which questions are displayed. + var displayedQuestions = null; + + // Dictionary of permission: whether we manage that permission. + var managedPermissions + + // Id of the item that's being lint-ed. + var lintedId = null; + + // Configuration of monitoring permissions. + var permissionCfg = [ + // Standard permissions. Detected by use of their api: chrome..* + {name: 'alarms', onActivity: detectPermissionStandard}, + {name: 'bookmarks', onActivity: detectPermissionStandard}, + {name: 'browsingData', onActivity: detectPermissionStandard}, + {name: 'contentSettings', onActivity: detectPermissionStandard}, + {name: 'contextMenus', onActivity: detectPermissionStandard}, + {name: 'debugger', onActivity: detectPermissionStandard}, + {name: 'downloads', onActivity: detectPermissionStandard}, + {name: 'fileBrowserHandler', onActivity: detectPermissionStandard}, + {name: 'fontSettings', onActivity: detectPermissionStandard}, + {name: 'history', onActivity: detectPermissionStandard}, + {name: 'identity', onActivity: detectPermissionStandard}, + {name: 'idle', onActivity: detectPermissionStandard}, + {name: 'notifications', onActivity: detectPermissionStandard}, + {name: 'pageCapture', onActivity: detectPermissionStandard}, + {name: 'power', onActivity: detectPermissionStandard}, + {name: 'privacy', onActivity: detectPermissionStandard}, + {name: 'proxy', onActivity: detectPermissionStandard}, + {name: 'pushMessaging', onActivity: detectPermissionStandard}, + {name: 'system.display', onActivity: detectPermissionStandard}, + {name: 'system.storage', onActivity: detectPermissionStandard}, + {name: 'tabCapture', onActivity: detectPermissionStandard}, + {name: 'topSites', onActivity: detectPermissionStandard}, + {name: 'tts', onActivity: detectPermissionStandard}, + {name: 'ttsEngine', onActivity: detectPermissionStandard}, + {name: 'webNavigation', onActivity: detectPermissionStandard}, + {name: 'webRequest', onActivity: detectPermissionStandard}, + + // Special permissions. Detection varies and is implemented in special + // functions. + { + // Only certain calls are guarded by the management permission. + name: 'management', + onActivity: detectPermissionManagement + }, + { + // Need to check if webRequest API was called with an argument 'blockign'. + name: 'webRequestBlocking', + onActivity: detectPermissionWebRequestBlocking + }, + { + // Tabs API is very complicated - need to ask the user and cherry-pick the + // functions that are called. + name: 'tabs', + onActivity: detectPermissionTabs, + triggersQuestion: 'tabs' + }, + { + // Need to ask the user because we can't detect document.execCommand(...). + name: 'clipboardRead', + onManifest: detectPermissionClipboard, + onQuestionAnswered: qaClipboard, + triggersQuestion: 'clipboard' + }, + { + // Need to ask the user because we can't detect document.execCommand(...). + name: 'clipboardWrite', + onManifest: detectPermissionClipboard, + onQuestionAnswered: qaClipboard, + triggersQuestion: 'clipboard' + }, + { + // Need to verify that the developer insists on getting the user's + // location without prompt. + name: 'geolocation', + onManifest: detectPermissionGeolocation, + onQuestionAnswered: qaGeolocation, + triggersQuestion: 'geolocation' + }, + { + // Need to verify that the developer uses localStorage and needs more than + // 5MB. + name: 'unlimitedStorage', + onManifest: detectPermissionUnlimitedStorage, + onQuestionAnswered: qaUnlimitedStorage, + triggersQuestion: 'unlimitedStorage' + }, + { + // Ask the developer whether he really intends to keep Chrome up all the + // time. + name: 'background', + onManifest: detectPermissionBackground, + onQuestionAnswered: qaBackground, + triggersQuestion: 'background' + }, + ]; + + /** + * Build an object that says whether we take notice of a particular + * permission or not. + * @param {!Object} lst List of permission objects. + * @private + */ + function buildManagedPermissions(lst) { + var res = {}; + lst.forEach(function(permCfg) { + res[permCfg.name] = true; + }); + return res; + } + + /** + * Figure out whether particular activity implies us of particular permission. + * @param {!Object} activity Activity from activityLogPrivate API + * @param {!string} perm Name of the permission under consideration + */ + function detectPermissionStandard(activity, perm) { + if(activity.activityType === "api_call") { + var regexp = new RegExp("^" + perm); + if(regexp.exec(activity.apiCall) !== null) { + return true; + } + } + return false; + } + + /** + * Figure out whether the management permission is implied by the given + * activity. + * @param {!Object} activity Activity from activityLogPrivate API + */ + function detectPermissionManagement(activity) { + if(activity.activityType === "api_call") { + var regexp = new RegExp("^management"); + if(regexp.exec(activity.apiCall) !== null) { + if(apiCall.indexOf("getPermissionWarningByManifest") !== 0 && + apiCall.indexOf("uninstallSelf") !== 0) { + return true; + } + } + } + return false; + } + + /** + * Figure out whether the webRequestBlocking permission is implied by the + * given activity. + * @param {!Object} activity Activity from activityLogPrivate API + */ + function detectPermissionWebRequestBlocking(activity) { + if(activity.activityType === "api_call") { + var apiCall = activity.apiCall; + if(apiCall === "webRequestInternal.addEventListener") { + var args = JSON.parse(activity.args); + var arg2 = args[2]; + if(arg2 instanceof Array) { + for(var i in arg2) { + if(arg2[i] === "blocking") + return true; + } + } + } + } + return false; + } + + function manifestPermissionGeneric(item, perm, question) { + if(item.permissions.indexOf(perm) !== -1) { + showQuestion(question); + } + } + + /** + * Process item's info from the perspective of geolocation permission. + * @param {!Object} item ItemInfo from chrome.management.get API + */ + function manifestPermissionGeolocation(item) { + // If the developer asked for geolocation permission, display a question. + manifestPermissionGeneric(item, 'geolocation', 'geolocation'); + } + + /** + * Process item's info from the perspective of clipboard* permissions. + * @param {!Object} item ItemInfo from chrome.management.get API + */ + function detectPermissionClipboard(permissions) { + // If the developer asked for clipboard permission, display a question. + manifestPermissionGeneric(item, 'clipboardRead', 'clipboard'); + manifestPermissionGeneric(item, 'clipboardWrite', 'clipboard'); + } + + /** + * Process item's info from the perspective of unlimitedStorage permission. + * @param {!Object} item ItemInfo from chrome.management.get API + */ + function detectPermissionUnlimitedStorage(permissions) { + // If the developer asked for unlimitedStorage permission, display + // a question. + manifestPermissionGeneric(item, 'geolocation', 'geolocation'); + } + + /** + * Process item's info from the perspective of background permission. + * @param {!Object} item ItemInfo from chrome.management.get API + */ + function detectPermissionBackground(permissions) { + // If the developer asked for background permission, display a question. + manifestPermissionGeneric(item, 'background', 'background'); + } + + /** + * Process answer to a question from the perspective of geolocation perm. + * @param {!string} questionId Identifier of the question. + * @param {!string} answer User's answer. + */ + function qaGeolocation(questionId, answer) { + if(questionId === 'geolocation') { + if(answer === 'yes') { + onPermissionDetected('geolocation'); + } + } + } + + /** + * Process answer to a question from the perspective of clipboard* perms. + * @param {!string} questionId Identifier of the question. + * @param {!string} answer User's answer. + */ + function qaClipboard(questionId, answer) { + if(questionId === 'clipboard') { + if(answer === 'copy') { + onPermissionDetected('clipboardWrite'); + } + else if(answer === 'paste') { + onPermissionDetected('clipboardRead'); + } + else if(answer === 'copypaste') { + onPermissionDetected('clipboardWrite'); + onPermissionDetected('clipboardRead'); + } + } + } + + /** + * Process answer to a question from the perspective of unlimitedStorage perm. + * @param {!string} questionId Identifier of the question. + * @param {!string} answer User's answer. + */ + function qaUnlimitedStorage(questionId, answer) { + if(questionId === 'unlimitedStorage') { + if(answer === 'yes') { + onPermissionDetected('unlimitedStorage'); + } + } + } + + /** + * Process answer to a question from the perspective of background perm. + * @param {!string} questionId Identifier of the question. + * @param {!string} answer User's answer. + */ + function qaBackground(questionId, answer) { + if(questionId === 'background') { + if(answer === 'yes') { + onPermissionDetected('background'); + } + } + } + + + /** + * Figure out whether the tabs permission is implied by the given activity. + * @param {!Object} activity Activity from activityLogPrivate API + */ + function detectPermissionTabs(activity) { + if(activity.activityType === "api_call") { + var regexp = new RegExp("^tabs"); + if(regexp.exec(activity.apiCall) !== null) { + if(apiCall.indexOf("query") !== 0 || + apiCall.indexOf("executeScript") !== 0 || + apiCall.indexOf("get") !== 0 || + apiCall.indexOf("getCurrent") !== 0 || + apiCall.indexOf("duplicate") !== 0 || + apiCall.indexOf("update") !== 0 || + apiCall.indexOf("move") !== 0 || + apiCall.indexOf("onCreated") !== 0 || + apiCall.indexOf("onUpdated") !== 0 || + apiCall.indexOf("executeScript") !== 0) { + return true; + } + } + } + return false; + } + + /** + * Display question for user. + * @param {!string} questionId Question identifier in the HTML template. + */ + function showQuestion(questionId) { + displayedQuestions[questionId] = true; + + // Locate the question element. + var questions = $('lintQuestions').children; + for(var i = 0; i < questions.length; i++) { + var question = questions[i]; + if(question.getAttribute('question-id') === questionId) { + // When located, unhide it and assign proper actions for click. + question.hidden = false; + var answers = question.querySelectorAll('[type="button"]'); + for(var i = 0; i < answers.length; i++) { + var answer = answers[i]; + answer.onclick = onQuestionAnswered.bind( + null, questionId, answer.getAttribute('q-answer')); + } + } + } + + // Display label if it wasn't displayed before. + updateQuestionLabel(); + } + + /** + * Hide question. + * @param {!string} questionId Question identifier in the HTML template. + */ + function hideQuestion(questionId) { + displayedQuestions[questionId] = false; + + // Locate the question element. + var questions = $('lintQuestions').children; + for(var i = 0; i < questions.length; i++) { + var question = questions[i]; + if(question.getAttribute('question-id') === questionId) { + // When located, hide it. + question.hidden = true; + } + } + + // Hide label if this is the last displayed question. + updateQuestionLabel(); + } + + /** + * Hide question label if no questions are displayed. + * @param {!string} questionId Question identifier in the HTML template. + */ + function updateQuestionLabel() { + var label = $('lintQuestionsLabel'); + for(var qId in displayedQuestions) { + if(displayedQuestions[qId] === true) { + label.hidden = false; + return; + } + } + label.hidden = true; + } + + /** + * Hide all questions. + */ + function hideAllQuestions() { + var questions = $('lintQuestions').children; + for(var i = 0; i < questions.length; i++) { + var question = questions[i]; + question.hidden = true; + } + } + + /** + * Handle chrome.activityLoggingPrivate.onExtensionActivity event by invoking + * corresponding handlers of each configured permission. + * @param {!object} activity chrome.activityLoggingPrivate API activity object + */ + function onExtensionActivity(activity) { + if(activity.extensionId === lintedId) { + permissionCfg.forEach(function(permCfg, i) { + if(permCfg.onActivity && permCfg.onActivity(activity, permCfg.name)) { + onPermissionDetected(permCfg.name); + } + }); + } + } + + /** + * Handle when the user answers an question by inovking corresponding handlers + * of each configured permission. + * @param {!string} questionId Identiifier of the question. + * @param {!string} answer User's answer to the question. + */ + function onQuestionAnswered(questionId, answer) { + permissionCfg.forEach(function(permCfg, i) { + if(permCfg.onQuestionAnswered && permCfg.onQuestionAnswered(questionId, answer, permCfg.name)) { + onPermissionDetected(permCfg.name); + } + }); + + hideQuestion(questionId); + } + + /** + * Handle the event of detecting a permission. + * @param {!string} permName Name of the permission that's been detected. + */ + function onPermissionDetected(permName) { + var originalValue = exercisedPermissions[permName]; + exercisedPermissions[permName] = true; + + // In case the permission was detected for the first time. + if(originalValue !== true) { + displayResults(); + } + } + + /** + * Delete all children of the element. + * @param {!Object} node Element. + */ + function clearElement(node) { + while (node.firstChild) { + node.removeChild(node.firstChild); + } + } + + /** + * Render results. + */ + function displayResults() { + // Assemble list of redundant permissions. + var redundantList = document.createElement('ul'); + requestedPermissions.forEach(function(perm) { + if(exercisedPermissions[perm] === undefined && + managedPermissions[perm] === true) { + var li = document.createElement('li'); + li.textContent = perm; + redundantList.appendChild(li); + } + }); + + var redundantPermNode = $('lintRedundantPermissions') + clearElement(redundantPermNode); + redundantPermNode.appendChild(redundantList); + + // Assemble list of used permissions. + var usedList = document.createElement('ul'); + for(var perm in exercisedPermissions) { + var li = document.createElement('li'); + li.textContent = perm; + usedList.appendChild(li); + } + + + var usedPermNode = $('lintUsedPermissions') + clearElement(usedPermNode); + usedPermNode.appendChild(usedList); + } + + /** + * Initialize the linter. + */ + function initialize() { + managedPermissions = buildManagedPermissions(permissionCfg); + }; + + + /** + * Starts security lint-ing the item + * @param {string} itemId Id of the Chrome item (app/extension). + * @doneCallback {function} doneCallback Function called when linting started. + */ + function start(itemId, doneCallback) { + lintedId = itemId; + exercisedPermissions = {}; + requestedPermissions = []; + displayedQuestions = {}; + + // Get information about the lint-ed item. + chrome.management.get(itemId, function(item) { + requestedPermissions = item.permissions; + + // Trigger onManifest events of each configured permission. + permissionCfg.forEach(function(permCfg, i) { + if(permCfg.onManifest && permCfg.onManifest(item)) { + onPermissionDetected(permCfg.name); + } + }); + }); + + // Start logging the activity of extenisons. + chrome.activityLogPrivate.onExtensionActivity.addListener( + onExtensionActivity); + + hideAllQuestions(); + + // Reload the extension (disable -> enable). + chrome.management.setEnabled(itemId, false, function() { + chrome.management.setEnabled(itemId, true, function() { + doneCallback(); + }); + }); + + }; + + /** + * Ends security lint-ing the item. + * @param {function} callback Function called when lining stopped. + */ + function stop(callback) { + chrome.activityLogPrivate.onExtensionActivity.removeListener( + onExtensionActivity); + callback(); + } + + // Export + return { + initialize: initialize, + start: start, + stop: stop, + }; +}); + +document.addEventListener('DOMContentLoaded', lintOverlay.initialize); diff --git a/js/items_list.js b/js/items_list.js index 3fca7fe..1f18337 100644 --- a/js/items_list.js +++ b/js/items_list.js @@ -261,6 +261,9 @@ cr.define('apps_dev_tool', function() { // Set delete button handler. this.setDeleteButton_(item, node); + // The 'Run with Security Lint' link. + this.setLintLink_(item, node); + // First get the item id. var idLabel = node.querySelector('.extension-id'); idLabel.textContent = ' ' + item.id; @@ -356,6 +359,27 @@ cr.define('apps_dev_tool', function() { }); }, + /** + * Sets the lint link handler. + * @param {!Object} item A dictionary of item metadata. + * @param {!HTMLElement} el HTML element containing all items. + * @private + */ + setLintLink_: function(item, el) { + var lint = el.querySelector('.lint-link'); + lint.addEventListener('click', function(e) { + lintOverlay.start(item.id, function() { + AppsDevTool.showOverlay($('lintOverlay')); + }); + }); + + $('lintCancel').addEventListener('click', function (e){ + lintOverlay.stop(function() { + AppsDevTool.showOverlay(null); + }); + }); + }, + /** * Sets the pack button handler. * @param {!Object} item A dictionary of item metadata. diff --git a/main.html b/main.html index 0265067..1e283c5 100644 --- a/main.html +++ b/main.html @@ -21,6 +21,7 @@ + @@ -48,6 +49,69 @@

+
+

+
+
+ +
+ + + + + +
+
+ +
+
+ + +
+
+
+ +
+
+

@@ -249,6 +313,8 @@

href="#" hidden> +
diff --git a/manifest.json b/manifest.json index c99a4bc..daac4be 100644 --- a/manifest.json +++ b/manifest.json @@ -12,7 +12,8 @@ }, "permissions": [ "management", - "developerPrivate" + "developerPrivate", + "activityLogPrivate" ], "icons": { "16": "images/icon-16.png", @@ -21,5 +22,6 @@ "128": "images/icon-128.png" }, "default_locale": "en", - "minimum_chrome_version": "31.0.1650.8" + "minimum_chrome_version": "31.0.1650.8", + "key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDV/GMX7sjLe3ceUizalvfZK0qhsWnXcjJ3cCbYvXFo43Q2F7SZM8/0roex0wSpNRSO1j9c/m7YXLYBAOiy21ERRJEVEIvOvWp1LLeoBSsbQnnhSPKInqUrkA8fMRCqI0gHRUK3K7dIiOC2A7jkWUMs4DqRiQSkntUUGzVIoY6OYQIDAQAB" } From f8fb1dd69f1fb208d021bbfa0c0b7ae4304ad196 Mon Sep 17 00:00:00 2001 From: Lukas Zilka Date: Fri, 6 Dec 2013 14:55:54 -0500 Subject: [PATCH 2/5] Fixing a bug with bad function names. --- js/chromium/lint_overlay.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/js/chromium/lint_overlay.js b/js/chromium/lint_overlay.js index 831b0a2..3fbc13a 100644 --- a/js/chromium/lint_overlay.js +++ b/js/chromium/lint_overlay.js @@ -70,14 +70,14 @@ cr.define('lintOverlay', function() { { // Need to ask the user because we can't detect document.execCommand(...). name: 'clipboardRead', - onManifest: detectPermissionClipboard, + onManifest: manifestPermissionClipboard, onQuestionAnswered: qaClipboard, triggersQuestion: 'clipboard' }, { // Need to ask the user because we can't detect document.execCommand(...). name: 'clipboardWrite', - onManifest: detectPermissionClipboard, + onManifest: manifestPermissionClipboard, onQuestionAnswered: qaClipboard, triggersQuestion: 'clipboard' }, @@ -85,7 +85,7 @@ cr.define('lintOverlay', function() { // Need to verify that the developer insists on getting the user's // location without prompt. name: 'geolocation', - onManifest: detectPermissionGeolocation, + onManifest: manifestPermissionGeolocation, onQuestionAnswered: qaGeolocation, triggersQuestion: 'geolocation' }, @@ -93,7 +93,7 @@ cr.define('lintOverlay', function() { // Need to verify that the developer uses localStorage and needs more than // 5MB. name: 'unlimitedStorage', - onManifest: detectPermissionUnlimitedStorage, + onManifest: manifestPermissionUnlimitedStorage, onQuestionAnswered: qaUnlimitedStorage, triggersQuestion: 'unlimitedStorage' }, @@ -101,7 +101,7 @@ cr.define('lintOverlay', function() { // Ask the developer whether he really intends to keep Chrome up all the // time. name: 'background', - onManifest: detectPermissionBackground, + onManifest: manifestPermissionBackground, onQuestionAnswered: qaBackground, triggersQuestion: 'background' }, @@ -195,7 +195,7 @@ cr.define('lintOverlay', function() { * Process item's info from the perspective of clipboard* permissions. * @param {!Object} item ItemInfo from chrome.management.get API */ - function detectPermissionClipboard(permissions) { + function manifestPermissionClipboard(item) { // If the developer asked for clipboard permission, display a question. manifestPermissionGeneric(item, 'clipboardRead', 'clipboard'); manifestPermissionGeneric(item, 'clipboardWrite', 'clipboard'); @@ -205,7 +205,7 @@ cr.define('lintOverlay', function() { * Process item's info from the perspective of unlimitedStorage permission. * @param {!Object} item ItemInfo from chrome.management.get API */ - function detectPermissionUnlimitedStorage(permissions) { + function manifestPermissionUnlimitedStorage(item) { // If the developer asked for unlimitedStorage permission, display // a question. manifestPermissionGeneric(item, 'geolocation', 'geolocation'); @@ -215,7 +215,7 @@ cr.define('lintOverlay', function() { * Process item's info from the perspective of background permission. * @param {!Object} item ItemInfo from chrome.management.get API */ - function detectPermissionBackground(permissions) { + function manifestPermissionBackground(item) { // If the developer asked for background permission, display a question. manifestPermissionGeneric(item, 'background', 'background'); } From e0a94ded5e3a1eabab5d29651045adb2e600007d Mon Sep 17 00:00:00 2001 From: Lukas Zilka Date: Fri, 6 Dec 2013 14:57:23 -0500 Subject: [PATCH 3/5] Fixing a message. --- _locales/en/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 9a1f2ef..f423a08 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -296,7 +296,7 @@ "description": "Text of the button in the linting dialog that triggers ending of linting." }, "lintRedundantPermissions": { - "message": "Redundant permissions", + "message": "These permissions are requested but never used", "description": "Title of a list with redundant permissions." }, "lintUsedPermissions": { From f487b1dea70eb4e8c33cd3110836a5002b2eb5c1 Mon Sep 17 00:00:00 2001 From: Lukas Zilka Date: Fri, 6 Dec 2013 14:57:47 -0500 Subject: [PATCH 4/5] Fixing a message. --- _locales/en/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index f423a08..7f04ba0 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -304,7 +304,7 @@ "description": "Title of a list with used permissions." }, "lintQuestions": { - "message": "Clarify", + "message": "Answer", "description": "Title of a list with questions." } } From a48cf68095d6aca3fb8f44f3d87488510afa6911 Mon Sep 17 00:00:00 2001 From: Lukas Zilka Date: Fri, 13 Dec 2013 18:12:54 -0500 Subject: [PATCH 5/5] adding content security policy and host permissions --- _locales/en/messages.json | 16 +- css/items.css | 12 ++ js/chromium/lint_overlay.js | 316 +++++++++++++++++++++++++++++++++--- main.html | 21 +-- manifest.json | 4 +- 5 files changed, 335 insertions(+), 34 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 7f04ba0..510d47a 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -284,11 +284,11 @@ "description": "Content of the dialog for managed profile. It's informing a supervised user that extensions cannot be changed." }, "lintStart": { - "message": "Please interact with the app or extension to exercise its full functionality. In the meanwhile the list of redundant permissions and used permissions will be updated.", + "message": "Please interact with the app or extension to exercise its full functionality. Security Lint will assess the permissions and web-resources needs of your extension. The linting results are live-updated below.", "description": "Content of the dialog displayed when the user runs an item with security lint." }, "lintStartTitle": { - "message": "Security Lint is now running", + "message": "Security Lint", "description": "Title of the dialog displayed when the user runs an item with security lint." }, "lintShowManifest": { @@ -304,7 +304,15 @@ "description": "Title of a list with used permissions." }, "lintQuestions": { - "message": "Answer", - "description": "Title of a list with questions." + "message": "To get better results answer some questions", + "description": "Title of a button with questions." + }, + "lintCSPSuggestions": { + "message": "Advised Content Security Policy", + "description": "Title of the item with CSP suggestions." + }, + "lintResults": { + "message": "Results of linting", + "description": "Title of the results section." } } diff --git a/css/items.css b/css/items.css index 8831ad7..39dacde 100644 --- a/css/items.css +++ b/css/items.css @@ -571,3 +571,15 @@ button[disabled]:active { #browse-private-key { margin-left: 10px; } + +#linting-results { + padding-bottom: 10px; + border-bottom: 1px solid #d1d1d1; + width: calc(100% - 40px); + font-weight: bold; +} + +#lintRedundantPermissions ul { + list-style: none; + color: red; +} diff --git a/js/chromium/lint_overlay.js b/js/chromium/lint_overlay.js index 3fbc13a..f22dde5 100644 --- a/js/chromium/lint_overlay.js +++ b/js/chromium/lint_overlay.js @@ -65,6 +65,7 @@ cr.define('lintOverlay', function() { // functions that are called. name: 'tabs', onActivity: detectPermissionTabs, + onQuestionAnswered: qaTabs, triggersQuestion: 'tabs' }, { @@ -105,6 +106,14 @@ cr.define('lintOverlay', function() { onQuestionAnswered: qaBackground, triggersQuestion: 'background' }, + { + // Meta permission for triggering code that detects host permissions and + // content security policy. + name: '', + onManifest: manifestHostAndCSP, + onNetworkEvent: networkEventHostAndCSP, + onActivity: activityHostAndCSP + } ]; /** @@ -176,6 +185,13 @@ cr.define('lintOverlay', function() { return false; } + /** + * Helper function that displays given question if the extension asked for + * the given permission. + * @param {!Object} item ItemInfo from chrome.management.get API + * @param {!string} param Name of the permission to check + * @param {!string} question ID of the question to show + */ function manifestPermissionGeneric(item, perm, question) { if(item.permissions.indexOf(perm) !== -1) { showQuestion(question); @@ -191,6 +207,15 @@ cr.define('lintOverlay', function() { manifestPermissionGeneric(item, 'geolocation', 'geolocation'); } + /** + * Process item's info from the perspective of host permission and CSP. + * @param {!Object} item ItemInfo from chrome.management.get API + */ + function manifestHostAndCSP(item) { + // Should process item.hostPermissions here. + return false; + } + /** * Process item's info from the perspective of clipboard* permissions. * @param {!Object} item ItemInfo from chrome.management.get API @@ -266,6 +291,19 @@ cr.define('lintOverlay', function() { } } + /** + * Process answer to a question from the perspective of tabs perm. + * @param {!string} questionId Identifier of the question. + * @param {!string} answer User's answer. + */ + function qaTabs(questionId, answer) { + if(questionId === 'tabs') { + if(answer === 'yes') { + onPermissionDetected('tabs'); + } + } + } + /** * Process answer to a question from the perspective of background perm. * @param {!string} questionId Identifier of the question. @@ -288,23 +326,62 @@ cr.define('lintOverlay', function() { if(activity.activityType === "api_call") { var regexp = new RegExp("^tabs"); if(regexp.exec(activity.apiCall) !== null) { - if(apiCall.indexOf("query") !== 0 || - apiCall.indexOf("executeScript") !== 0 || - apiCall.indexOf("get") !== 0 || - apiCall.indexOf("getCurrent") !== 0 || - apiCall.indexOf("duplicate") !== 0 || - apiCall.indexOf("update") !== 0 || - apiCall.indexOf("move") !== 0 || - apiCall.indexOf("onCreated") !== 0 || - apiCall.indexOf("onUpdated") !== 0 || - apiCall.indexOf("executeScript") !== 0) { - return true; + var apiCall = activity.apiCall; + if(apiCall.indexOf("query") !== -1 || + apiCall.indexOf("executeScript") !== -1 || + apiCall.indexOf("get") !== -1 || + apiCall.indexOf("getCurrent") !== -1 || + apiCall.indexOf("duplicate") !== -1 || + apiCall.indexOf("update") !== -1 || + apiCall.indexOf("move") !== -1 || + apiCall.indexOf("onCreated") !== -1 || + apiCall.indexOf("onUpdated") !== -1 || + apiCall.indexOf("executeScript") !== -1) { + if(requestedPermissions.indexOf("tabs") !== -1) + showQuestion("tabs"); + return false; } } } return false; } + /** + * Get url from the given activity. + * @param {!Object} activity Activity from activityLogPrivate API + */ + function getActivityArgUrl(activity, ndx) { + var args = JSON.parse(activity.args); + var url = args[ndx]; + if(url === "") { + url = activity.argUrl; + } + return url; + } + + /** + * Process the activity from the standpoint of host permissions and CSP. + * @param {!Object} activity Activity from activityLogPrivate API + */ + function activityHostAndCSP(activity) { + if(activity.activityType === "dom_access") { + if(activity.apiCall === "XMLHttpRequest.open") { + var url = getActivityArgUrl(activity, 1); + onWebResourceDetected(url, "XHR"); + } + } + return false; + } + + /** + * Process network event from chrome.debugger API from the standpoint of + * host permissions and CSP. + */ + function networkEventHostAndCSP(req) { + onWebResourceDetected(req.url, req.type); + return false; + } + /** * Display question for user. * @param {!string} questionId Question identifier in the HTML template. @@ -320,8 +397,8 @@ cr.define('lintOverlay', function() { // When located, unhide it and assign proper actions for click. question.hidden = false; var answers = question.querySelectorAll('[type="button"]'); - for(var i = 0; i < answers.length; i++) { - var answer = answers[i]; + for(var y = 0; y < answers.length; y++) { + var answer = answers[y]; answer.onclick = onQuestionAnswered.bind( null, questionId, answer.getAttribute('q-answer')); } @@ -377,6 +454,7 @@ cr.define('lintOverlay', function() { var question = questions[i]; question.hidden = true; } + updateQuestionLabel(); } /** @@ -394,6 +472,28 @@ cr.define('lintOverlay', function() { } } + /** + * Handle chrome.debugger.onEvent event by invoking the corresponding handlers + * of each configured permission. + * @param {!object} chrome.debugger.onEvent API event object + */ + function onNetworkEvent(tab, message, params) { + if(message === "Network.responseReceived") { + if(params.type !== "XHR") { // XHRs are captured in onActivity + var req = { + url: params.response.url, + type: params.type + }; + permissionCfg.forEach(function(permCfg, i) { + if(permCfg.onNetworkEvent && + permCfg.onNetworkEvent(req, permCfg.name)) { + onPermissionDetected(permCfg.name); + } + }); + } + } + } + /** * Handle when the user answers an question by inovking corresponding handlers * of each configured permission. @@ -410,18 +510,88 @@ cr.define('lintOverlay', function() { hideQuestion(questionId); } + /** + * Get host location from the given url. + * @param {!string} href URL + */ + function getLocation(href) { + var l = document.createElement("a"); + l.href = href; + return l; + } + + /** + * Extract CSP location string from the url. + * @param {!string} url URL + */ + function extractCSPHost(url) { + var loc = getLocation(url); + var host = loc.protocol + '//' + loc.hostname; + return host; + } + + /** + * Extract host permission from given URL. + * @param {!string} url URL + */ + function extractPermHost(url) { + return extractCSPHost(url) + "/*"; + } + + /** + * Handle the event of detecting a web resource. + * @param {!string} url URL of the resource + * @param {!string} type Type of the resource + */ + function onWebResourceDetected(url, type) { + if(type === "XHR") { + var hostPerm = extractPermHost(url); + if(hostPerm.indexOf("chrome-extension://") !== 0) + onHostPermissionDetected(hostPerm); + + var hostCSP = extractCSPHost(url); + if(hostCSP.indexOf("chrome-extension://") !== 0) + onCSPDetected(hostCSP, "connect"); + else + onCSPDetected("self", "connect"); + } else { + var hostCSP = extractCSPHost(url); + if(hostCSP.indexOf("chrome-extension://") !== 0) + onCSPDetected(hostCSP, "default"); + else + onCSPDetected("self", "default"); + } + } + /** * Handle the event of detecting a permission. * @param {!string} permName Name of the permission that's been detected. */ function onPermissionDetected(permName) { - var originalValue = exercisedPermissions[permName]; + console.debug("permission detected: " + permName); exercisedPermissions[permName] = true; - // In case the permission was detected for the first time. - if(originalValue !== true) { - displayResults(); - } + displayResults(); + } + + /** + * Handle the event of detecting a host permission. + * @param {!string} permName Name of the permission that's been detected. + */ + function onHostPermissionDetected(permName) { + exercisedHostPermissions[permName] = true; + + displayResults(); + } + + /** + * Handle the event of detecting a CSP. + * @param {!string} permName Name of the permission that's been detected. + */ + function onCSPDetected(host, type) { + exercisedCSP[type][host] = true; + + displayResults(); } /** @@ -453,6 +623,19 @@ cr.define('lintOverlay', function() { clearElement(redundantPermNode); redundantPermNode.appendChild(redundantList); + // Assemble CSP suggestion. + var cspSuggestionNode = $('lintCSPSuggestion'); + var cspStr = " 'content_security_policy': "; + for(var cspType in exercisedCSP) { + cspStr += cspType + "-src "; + for(var cspHost in exercisedCSP[cspType]) { + cspStr += cspHost + " "; + } + cspStr += "; "; + } + cspSuggestionNode.innerText = cspStr; + + // Assemble list of used permissions. var usedList = document.createElement('ul'); for(var perm in exercisedPermissions) { @@ -472,8 +655,75 @@ cr.define('lintOverlay', function() { */ function initialize() { managedPermissions = buildManagedPermissions(permissionCfg); + + $('lintQuestionsLabel').onclick = function() { + $('lintQuestions').hidden = ! $('lintQuestions').hidden; + }; }; + /** + * Handle the onAttach event. + * @param{!int} tabId ID of the tab to attach debugger to + */ + function onAttach(tabId) { + if (chrome.runtime.lastError) { + console.debug(chrome.runtime.lastError.message); + return; + } + + chrome.debugger.sendCommand({tabId:tabId}, "Network.enable"); + console.debug("attached " + tabId); + } + + /** + * Handle the onAttach event. + * @param{!int} eId ID of the extension to attach debugger to + */ + function onAttachExtension(eId) { + if (chrome.runtime.lastError) { + var msg = chrome.runtime.lastError.message; + if(msg.indexOf("silent-debugger-extension-api") !== -1) { + $("#error").html("Please go to chrome://flags and enable silent-debugger-extension-api. Then restart your browser and try again.").show(); + } + console.debug(msg); + return; + } + + var debugeeId = {extensionId: eId}; + chrome.debugger.sendCommand(debugeeId, "Network.enable", function() { + if(chrome.runtime.lastError) + console.debug("Failed enabling network for extension: " + chrome.runtime.lastError.message); + else + console.debug("dbugger nbled"); + }); + } + + /** + * Attach the debugger to given tab. + * @param{!int} tabId ID of the tab to attach debugger to. + */ + function attachDebugger(tab) { + console.debug("attaching"); + chrome.debugger.attach({tabId:tab.id}, "1.0", + onAttach.bind(null, tab.id)); + } + + /** + * Attach the debugger to the given extension background page. + * @param{!string} eId ID of the extension to attach to + */ + function attachDebuggerToExtension(eid) { + chrome.debugger.attach({extensionId: eid}, "1.0", + onAttachExtension.bind(null, eid)); + } + + /** + * Get extension's url. + * @param{!string} eId extension's id + */ + function getExtensionUrl(eId) { + return "chrome-extension://" + eId + "/"; + } /** * Starts security lint-ing the item @@ -483,12 +733,20 @@ cr.define('lintOverlay', function() { function start(itemId, doneCallback) { lintedId = itemId; exercisedPermissions = {}; + exercisedHostPermissions = {}; requestedPermissions = []; + requestedHostPermissions = []; + attachedTo = []; + exercisedCSP = { + connect: {}, + default: {} + }; displayedQuestions = {}; // Get information about the lint-ed item. chrome.management.get(itemId, function(item) { requestedPermissions = item.permissions; + requestedHostPermissions = item.hostPermissions; // Trigger onManifest events of each configured permission. permissionCfg.forEach(function(permCfg, i) { @@ -496,6 +754,9 @@ cr.define('lintOverlay', function() { onPermissionDetected(permCfg.name); } }); + + // Refresh view. + displayResults(); }); // Start logging the activity of extenisons. @@ -504,9 +765,21 @@ cr.define('lintOverlay', function() { hideAllQuestions(); + // Attach debugger to the extension's background page. + chrome.debugger.onEvent.addListener(onNetworkEvent); + + // If a new tab is created belonging to the extension, attach debugger. + chrome.tabs.onCreated.addListener(function(tab) { + if(tab.url.indexOf(getExtensionUrl(itemId)) === 0) { + attachDebugger(tab); + attachedTo.push(tab.id); + } + }); + // Reload the extension (disable -> enable). chrome.management.setEnabled(itemId, false, function() { chrome.management.setEnabled(itemId, true, function() { + attachDebuggerToExtension(itemId); doneCallback(); }); }); @@ -518,6 +791,11 @@ cr.define('lintOverlay', function() { * @param {function} callback Function called when lining stopped. */ function stop(callback) { + chrome.debugger.detach({extensionId: itemId}); + for(var i in attachedTo) { + chrome.debugger.detach({tabId: attachedTo[i]}); + } + chrome.activityLogPrivate.onExtensionActivity.removeListener( onExtensionActivity); callback(); @@ -527,7 +805,7 @@ cr.define('lintOverlay', function() { return { initialize: initialize, start: start, - stop: stop, + stop: stop }; }); diff --git a/main.html b/main.html index 1e283c5..a301994 100644 --- a/main.html +++ b/main.html @@ -53,10 +53,12 @@

- -
+ +

+ + + +

-
- -
+

+

+

+          

- +
diff --git a/manifest.json b/manifest.json index daac4be..b7ef9f0 100644 --- a/manifest.json +++ b/manifest.json @@ -13,7 +13,9 @@ "permissions": [ "management", "developerPrivate", - "activityLogPrivate" + "activityLogPrivate", + "debugger", + "tabs" ], "icons": { "16": "images/icon-16.png",