зеркало из https://github.com/mozilla/OriginOnly.git
initial revision
This commit is contained in:
Коммит
cb9d9cffb9
|
@ -0,0 +1,11 @@
|
|||
This is a Firefox addon to demonstrate the cookie flag CSRF prevention mechanism.
|
||||
|
||||
Questions I’ve been asked so far:
|
||||
|
||||
How does it work?
|
||||
The mechanism consists of a new cookie flag (tentatively called OriginOnly) which, when set, instructs the browser to only send the cookie when the cookie domain attribute matches the domain of the referring URI#. Aside from this restriction, browsesr should behave exactly as they would otherwise.
|
||||
|
||||
Why is this better than server-side CSRF protection?
|
||||
It might not be (CSRF tokens that are tied to the users’ session identifiers are quite safe) but sane CSRF protection mechanisms do not exist, by default, in all frameworks and when they do they are often disabled by developers. This mechanisms would provide a safety mechanism that would protect users in the case of flawed or non-existent server CSRF mechanisms and provide a way to protect legacy applications from future attacks.
|
||||
|
||||
One benefit that this mechanism does provide comes from a side effect of the fact that this check is performed by the browser for all requests; not just POSTs where we intend to modify data or perform operations: Because the requests from most reflected XSS attacks would look like forged requests to this mechanism, for pages behind auth, the vulnerable resource may not be reachable.
|
|
@ -0,0 +1,3 @@
|
|||
This is the OriginOnly add-on. It contains:
|
||||
|
||||
* A program (lib/main.js).
|
|
@ -0,0 +1,2 @@
|
|||
The main module is a program that creates a widget. When a user clicks on
|
||||
the widget, the program loads the mozilla.org website in a new tab.
|
Двоичный файл не отображается.
Двоичный файл не отображается.
|
@ -0,0 +1,302 @@
|
|||
// Import the APIs we need.
|
||||
//var request = require("request");
|
||||
const {Cc,Ci} = require("chrome");
|
||||
var ss = require("simple-storage");
|
||||
var ch = Components.classes["@mozilla.org/security/hash;1"].createInstance(Components.interfaces.nsICryptoHash);
|
||||
var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
|
||||
converter.charset = "UTF-8";
|
||||
var store = ss.storage.store;
|
||||
var data = [];
|
||||
if(store){
|
||||
data = JSON.parse(store);
|
||||
}
|
||||
|
||||
String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g, ''); }
|
||||
|
||||
var listener = null;
|
||||
var flagStore = null;
|
||||
|
||||
// Cookie stuff
|
||||
var showCookieDebug = function(c,host){
|
||||
console.log("cookie named: "+c.name+" path "+c.path+" secure: "+c.isSecure+' host '+c.host+' (host is '+host+')');
|
||||
};
|
||||
|
||||
var checkCookieHost = function(c,host){
|
||||
var domainPrefix = '.';
|
||||
if(c.host.slice(0, domainPrefix.length) == domainPrefix){
|
||||
if(c.host.substring(1) != host){
|
||||
if(host.slice(-c.host.length) != c.host){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(host!=c.host){
|
||||
if(0==host.length){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Storage stuff
|
||||
function FlagStore(){
|
||||
this.makeStorageKey = function(domain,name,path){
|
||||
ch.init(ch.SHA256);
|
||||
var plainKey = domain + ':'+name;
|
||||
var result = {};
|
||||
var keyData = converter.convertToByteArray(plainKey, result);
|
||||
ch.update(keyData, keyData.length);
|
||||
var hash = ch.finish(false);
|
||||
// return the two-digit hexadecimal code for a byte
|
||||
function toHexString(charCode)
|
||||
{
|
||||
return ("0" + charCode.toString(16)).slice(-2);
|
||||
}
|
||||
|
||||
// convert the binary hash data to a hex string.
|
||||
var hashed = [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
|
||||
return hashed;
|
||||
};
|
||||
|
||||
this.cookieIsOriginRestricted = function(cookie){
|
||||
var storageKey = this.makeStorageKey(cookie.host,cookie.name,cookie.path);
|
||||
if(-1!=data.indexOf(storageKey)){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
this.storeOriginOnlyData = function(header, host){
|
||||
var elements = header.split(';');
|
||||
var cookieName = '';
|
||||
var soo = false;
|
||||
var pairs = {};
|
||||
for(key in elements){
|
||||
var trimmed = elements[key].trim();
|
||||
if(trimmed.toLowerCase()=='originonly'){
|
||||
soo = true;
|
||||
}
|
||||
idx = trimmed.indexOf('=');
|
||||
if(-1!=idx){
|
||||
var name = trimmed.substring(0,idx).trim();
|
||||
var value = trimmed.substring(idx+1).trim();
|
||||
pairs[name]=value;
|
||||
if(0==key){
|
||||
cookieName = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(soo){
|
||||
try {
|
||||
var path = pairs['path'];
|
||||
var domain = pairs['domain'];
|
||||
if(!domain){
|
||||
domain = host;
|
||||
}
|
||||
var storageKey = this.makeStorageKey(domain,cookieName,path);
|
||||
data[data.length] = storageKey;
|
||||
stored = JSON.stringify(data);
|
||||
ss.storage.store = stored;
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function MatchCookie(name,value){
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
|
||||
this.matches = function(cookie){
|
||||
if(this.name!=cookie.name){
|
||||
return false;
|
||||
}
|
||||
if(this.value!=cookie.value){
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
// Build cookie string stuff
|
||||
function CookieBuilder(cookieString, aChannel){
|
||||
this.channel = aChannel;
|
||||
this.cookieString = cookieString;
|
||||
|
||||
this.getOriginalCookies = function(){
|
||||
cookies = [];
|
||||
var elements = this.cookieString.trim().split(';');
|
||||
for(e in elements){
|
||||
element = elements[e];
|
||||
idx = element.indexOf('=');
|
||||
if(-1!=idx){
|
||||
var name = element.substring(0,idx).trim();
|
||||
var value = element.substring(idx+1).trim();
|
||||
cookies[cookies.length] = new MatchCookie(name,value);
|
||||
}
|
||||
}
|
||||
return cookies;
|
||||
}
|
||||
|
||||
this.originalCookies = this.getOriginalCookies();
|
||||
|
||||
this.cookieIsOriginal = function(cookie){
|
||||
for(i in this.originalCookies){
|
||||
if(this.originalCookies[i].matches(cookie)){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
this.buildCookies = function(){
|
||||
var host = this.channel.URI.host;
|
||||
var cookieMgr = Components.classes["@mozilla.org/cookiemanager;1"].getService(Components.interfaces.nsICookieManager2);
|
||||
|
||||
var enm = cookieMgr.getCookiesFromHost(host);
|
||||
var sep = '';
|
||||
var cs = '';
|
||||
|
||||
while (enm.hasMoreElements())
|
||||
{
|
||||
cookie = enm.getNext().QueryInterface(Components.interfaces.nsICookie);
|
||||
try{
|
||||
|
||||
// Is the cookie name / value in the original browser cookie string? If not, bin the cookie
|
||||
if(!this.cookieIsOriginal(cookie)){
|
||||
// not in whitelist, ignore
|
||||
continue;
|
||||
}
|
||||
|
||||
var referrer = this.channel.referrer;
|
||||
// sometimes nsIHttpChannel.referrer is not set for security reasons
|
||||
// if this is the case, dig it out of docshell properties
|
||||
if(null == referrer){
|
||||
var intRef = null;
|
||||
try {
|
||||
if (this.channel instanceof Ci.nsIPropertyBag2){
|
||||
intRef = this.channel.getPropertyAsInterface("docshell.internalReferrer", Ci.nsIURI);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
if(null!=intRef){
|
||||
// if it's a cache URL, find the URL of the original resource
|
||||
if(intRef.scheme == 'wyciwyg'){
|
||||
path = intRef.path;
|
||||
url = path.substr(path.indexOf('http'));
|
||||
var ioService = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
|
||||
intRef = ioService.newURI(url, null, null);
|
||||
referrer = intRef;
|
||||
}
|
||||
// URI host will be null, but we don't want to allow file URLs.
|
||||
if(intRef.scheme == 'file'){
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(referrer){
|
||||
if(!checkCookieHost(cookie,referrer.host)){
|
||||
if(flagStore.cookieIsOriginRestricted(cookie)){
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e){
|
||||
console.log('cookie build failed: '+e);
|
||||
}
|
||||
cs = cs+sep+cookie.name+'='+cookie.value;
|
||||
sep = '; ';
|
||||
}
|
||||
return cs;
|
||||
};
|
||||
}
|
||||
|
||||
// Header visitor for finding Set-Cookie headers
|
||||
function Visitor(host){
|
||||
this.host = host;
|
||||
|
||||
this.visitHeader = function ( aHeader, aValue ) {
|
||||
if ( aHeader.indexOf( "Set-Cookie" ) !== -1 ) {
|
||||
flagStore.storeOriginOnlyData(aValue,this.host);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Listener for nsIHttpChannel activity
|
||||
function Listener(){
|
||||
}
|
||||
|
||||
Listener.prototype = {
|
||||
observe: function(aChannel, aTopic, aData) {
|
||||
aChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
|
||||
visitor = new Visitor(aChannel.URI.host);
|
||||
|
||||
url = aChannel.URI.spec;
|
||||
if(aTopic == 'http-on-examine-response'){
|
||||
try{
|
||||
// TODO: Having our FlagStore listening for cookie-changed events is the way to go here
|
||||
aChannel.visitResponseHeaders(visitor);
|
||||
}
|
||||
catch(e){
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
if(aTopic == 'http-on-modify-request'){
|
||||
try{
|
||||
var cookieSvc = Components.classes["@mozilla.org/cookieService;1"]
|
||||
.getService(Components.interfaces.nsICookieService);
|
||||
//var cookieString = cookieSvc.getCookieString(aChannel.URI, aChannel);
|
||||
var actualHeader = '';
|
||||
try{
|
||||
actualHeader = aChannel.getRequestHeader('Cookie');
|
||||
} catch (e){
|
||||
// meh
|
||||
}
|
||||
if(actualHeader){
|
||||
var cookieBuilder = new CookieBuilder(actualHeader, aChannel);
|
||||
var built = cookieBuilder.buildCookies();
|
||||
if(actualHeader!=built){
|
||||
aChannel.setRequestHeader('Cookie',built,false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(e){
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
removeFromListener: function() {
|
||||
var observerService = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService);
|
||||
observerService.removeObserver(this, "http-on-modify-request");
|
||||
observerService.removeObserver(this, "http-on-examine-response");
|
||||
},
|
||||
|
||||
addToListener: function() {
|
||||
// Register new request and response listener
|
||||
// Should be a new version of Mozilla/Phoenix (after september 15, 2003)
|
||||
var observerService = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService);
|
||||
observerService.addObserver(this, "http-on-modify-request", false);
|
||||
observerService.addObserver(this, "http-on-examine-response", false);
|
||||
}
|
||||
};
|
||||
|
||||
exports.main = function(options, callbacks) {
|
||||
listener = new Listener();
|
||||
flagStore = new FlagStore();
|
||||
listener.addToListener();
|
||||
};
|
||||
|
||||
exports.onUnload = function (reason) {
|
||||
if(listener){
|
||||
listener.removeFromListener();
|
||||
}
|
||||
console.log(reason);
|
||||
};
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "originonly",
|
||||
"license": "MPL 1.1/GPL 2.0/LGPL 2.1",
|
||||
"author": "Mark Goodwin",
|
||||
"version": "0.1",
|
||||
"fullName": "OriginOnly",
|
||||
"id": "jid1-oJx4G1k5C3KxQA",
|
||||
"description": "a simple addon to demonstrate the anti-CSRF cookie flag idea"
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
const main = require("main");
|
||||
|
||||
exports.test_test_run = function(test) {
|
||||
test.pass("Unit test running!");
|
||||
};
|
||||
|
||||
exports.test_id = function(test) {
|
||||
test.assert(require("self").id.length > 0);
|
||||
};
|
||||
|
||||
exports.test_url = function(test) {
|
||||
require("request").Request({
|
||||
url: "http://www.mozilla.org/",
|
||||
onComplete: function(response) {
|
||||
test.assertEqual(response.statusText, "OK");
|
||||
test.done();
|
||||
}
|
||||
}).get();
|
||||
test.waitUntilDone(20000);
|
||||
};
|
||||
|
||||
exports.test_open_tab = function(test) {
|
||||
const tabs = require("tabs");
|
||||
tabs.open({
|
||||
url: "http://www.mozilla.org/",
|
||||
onReady: function(tab) {
|
||||
test.assertEqual(tab.url, "http://www.mozilla.org/");
|
||||
test.done();
|
||||
}
|
||||
});
|
||||
test.waitUntilDone(20000);
|
||||
};
|
Загрузка…
Ссылка в новой задаче