From 0dbf5e54fc4b7e720564cca07868840c8b4ecdb8 Mon Sep 17 00:00:00 2001 From: Ben Woolley Date: Wed, 7 Jan 2015 17:01:32 -0800 Subject: [PATCH 5/5] same-origin policy --- config.def.h | 6 + originprompt.html | 176 +++++++++++++++++++++++++++ surf.c | 349 +++++++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 487 insertions(+), 44 deletions(-) create mode 100644 originprompt.html diff --git a/config.def.h b/config.def.h index 033adfe..7126115 100644 --- a/config.def.h +++ b/config.def.h @@ -6,6 +6,10 @@ static char *downloadfolder = "~/Downloads/"; static char *stylefile = "~/.surf/style.css"; static char *scriptfile = "~/.surf/script.js"; static char *cachefolder = "~/.surf/cache/"; +static char *originpromptfile = NULL; +static char *originpromptdigestsalt = "Surf Origin Prompt Digest Salt 123456789"; /* change me; changing invalidates the origin trust state */ +static char *originprompthmackey = "Surf Origin Prompt HMAC Key 123456789"; /* " */ +static char *origincachefolder = "~/.surf/origins/%s/cache/"; static Bool kioskmode = FALSE; /* Ignore shortcuts */ static Bool showindicators = TRUE; /* Show indicators in window title */ @@ -17,6 +21,7 @@ static gfloat zoomlevel = 1.0; /* Default zoom level */ /* Soup default features */ static char *cookiefile = "~/.surf/cookies.txt"; +static char *origincookiefile = "~/.surf/origins/%s/cookies.txt"; static char *cookiepolicies = "Aa@"; /* A: accept all; a: accept nothing, @: accept no third party */ static char *cafile = "/etc/ssl/certs/ca-certificates.crt"; @@ -34,6 +39,7 @@ static Bool enableinspector = TRUE; static Bool loadimages = TRUE; static Bool hidebackground = FALSE; static Bool allowgeolocation = TRUE; +static Bool sameoriginpolicy = TRUE; #define SETPROP(p, q) { \ .v = (char *[]){ "/bin/sh", "-c", \ diff --git a/originprompt.html b/originprompt.html new file mode 100644 index 0000000..629f577 --- /dev/null +++ b/originprompt.html @@ -0,0 +1,176 @@ + + + + + +Origin Crossing Gate + + +
+

Origin Crossing Gate

+ + + + + + + + + + + + + + +
Proceed with Caution
From Origin%s
To Origin%s
To URL%s
+ + + + + + + + +
Trust This Origin Crossing and Suppress This Prompt For
Duration1 Day1 Month1 Year
+ + + +; diff --git a/surf.c b/surf.c index 91a69ed..0ecbdbd 100644 --- a/surf.c +++ b/surf.c @@ -34,6 +34,7 @@ char *argv0; #define COOKIEJAR(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), COOKIEJAR_TYPE, CookieJar)) enum { AtomFind, AtomGo, AtomUri, AtomLast }; +enum NavTransparency { NavExplicit, NavImplicit }; typedef union Arg Arg; union Arg { @@ -78,11 +79,14 @@ static GdkNativeWindow embed = 0; static gboolean showxid = FALSE; static char winid[64]; static gboolean usingproxy = 0; -static char togglestat[9]; +static char togglestat[10]; static char pagestat[3]; static GTlsDatabase *tlsdb; static int policysel = 0; +static char *origin_uri = NULL; +static char *referring_origin = NULL; static SoupCache *diskcache = NULL; +static char *originprompt = NULL; static void addaccelgroup(Client *c); static void beforerequest(WebKitWebView *w, WebKitWebFrame *f, @@ -111,6 +115,9 @@ static WebKitWebView *createwindow(WebKitWebView *v, WebKitWebFrame *f, static gboolean decidedownload(WebKitWebView *v, WebKitWebFrame *f, WebKitNetworkRequest *r, gchar *m, WebKitWebPolicyDecision *p, Client *c); +static gboolean decidenavigation(WebKitWebView *v, WebKitWebFrame *f, + WebKitNetworkRequest *r, WebKitWebNavigationAction *n, + WebKitWebPolicyDecision *p, Client *c); static gboolean decidewindow(WebKitWebView *v, WebKitWebFrame *f, WebKitNetworkRequest *r, WebKitWebNavigationAction *n, WebKitWebPolicyDecision *p, Client *c); @@ -144,10 +151,18 @@ static void linkhover(WebKitWebView *v, const char* t, const char* l, Client *c); static void loadstatuschange(WebKitWebView *view, GParamSpec *pspec, Client *c); -static void loaduri(Client *c, const Arg *arg); +static void loadgate(Client *c, const char *cross_from_origin, const char *cross_to_uri); +static void loaduri(Client *c, const Arg *arg, const enum NavTransparency navtrans); static void navigate(Client *c, const Arg *arg); static Client *newclient(void); -static void newwindow(Client *c, const Arg *arg, gboolean noembed); +static void newwindow(Client *c, const Arg *arg, gboolean noembed, const enum NavTransparency navtrans); +static int origincmp(const char *uri1, const char *uri2); +static int originhas(const char *uri); +static int originhas(const char *uri); +static const char *origingetproto(const char *uri); +static char *origingethost(const char *uri); +static char *origingeturi(const char *uri); +static int originmatch(const char *uri1, const char *uri2); static void pasteuri(GtkClipboard *clipboard, const char *text, gpointer d); static gboolean contextmenu(WebKitWebView *view, GtkWidget *menu, WebKitHitTestResult *target, gboolean keyboard, Client *c); @@ -161,7 +176,7 @@ static void scroll_h(Client *c, const Arg *arg); static void scroll_v(Client *c, const Arg *arg); static void scroll(GtkAdjustment *a, const Arg *arg); static void setatom(Client *c, int a, const char *v); -static void setup(void); +static void setup(const char *uri_arg); static void sigchld(int unused); static void source(Client *c, const Arg *arg); static void spawn(Client *c, const Arg *arg); @@ -175,6 +190,7 @@ static void togglestyle(Client *c, const Arg *arg); static void updatetitle(Client *c); static void updatewinid(Client *c); static void usage(void); +char *qualify_uri(const char *uri); static void windowobjectcleared(GtkWidget *w, WebKitWebFrame *frame, JSContextRef js, JSObjectRef win, Client *c); static void zoom(Client *c, const Arg *arg); @@ -252,7 +268,7 @@ buttonrelease(WebKitWebView *web, GdkEventButton *e, GList *gl) { if(e->button == 2 || (e->button == 1 && CLEANMASK(e->state) == CLEANMASK(MODKEY))) { g_object_get(result, "link-uri", &arg.v, NULL); - newwindow(NULL, &arg, e->state & GDK_CONTROL_MASK); + newwindow(NULL, &arg, e->state & GDK_CONTROL_MASK, NavImplicit); return true; } } @@ -422,6 +438,35 @@ decidedownload(WebKitWebView *v, WebKitWebFrame *f, WebKitNetworkRequest *r, } static gboolean +decidenavigation(WebKitWebView *view, WebKitWebFrame *f, WebKitNetworkRequest *r, + WebKitWebNavigationAction *n, WebKitWebPolicyDecision *p, + Client *c) { + const char *uri = webkit_network_request_get_uri(r); + Arg arg; + + if (!sameoriginpolicy) { + /* configured to not bother isolating origins */ + return FALSE; + } else if (webkit_web_frame_get_parent(f)) { + /* has a parent, and therefore not an origin */ + return FALSE; + /* branches below operate on the origin, top-most frame */ + } else if (uri && (uri[0] == '\0' || strcmp(uri, "about:blank") == 0)) { + /* nothing is really going to load */ + return FALSE; + } else if (origin_uri == NULL || originmatch(uri, origin_uri)) { + /* origin matches */ + return FALSE; + } else { + /* top-most frame, and origin differs -- isolate within a new process */ + webkit_web_policy_decision_ignore(p); + arg.v = (void *) uri; + newwindow(NULL, &arg, 0, NavImplicit); + return TRUE; + } +} + +static gboolean decidewindow(WebKitWebView *view, WebKitWebFrame *f, WebKitNetworkRequest *r, WebKitWebNavigationAction *n, WebKitWebPolicyDecision *p, Client *c) { @@ -431,7 +476,7 @@ decidewindow(WebKitWebView *view, WebKitWebFrame *f, WebKitNetworkRequest *r, WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED) { webkit_web_policy_decision_ignore(p); arg.v = (void *)webkit_network_request_get_uri(r); - newwindow(NULL, &arg, 0); + newwindow(NULL, &arg, 0, NavImplicit); return TRUE; } return FALSE; @@ -534,7 +579,7 @@ geturi(Client *c) { char *uri; if(!(uri = (char *)webkit_web_view_get_uri(c->view))) - uri = "about:blank"; + uri = ""; return uri; } @@ -640,6 +685,9 @@ loadstatuschange(WebKitWebView *view, GParamSpec *pspec, Client *c) { switch(webkit_web_view_get_load_status (c->view)) { case WEBKIT_LOAD_COMMITTED: uri = geturi(c); + if (strcmp(uri, "about:blank") != 0) { + origin_uri = uri; + } if(strstr(uri, "https://") == uri) { frame = webkit_web_view_get_main_frame(c->view); src = webkit_web_frame_get_data_source(frame); @@ -663,38 +711,63 @@ loadstatuschange(WebKitWebView *view, GParamSpec *pspec, Client *c) { } } -static void -loaduri(Client *c, const Arg *arg) { - char *u = NULL, *rp; - const char *uri = (char *)arg->v; - Arg a = { .b = FALSE }; +/* needs to be g_free()'d by caller */ +char * +qualify_uri(const char *uri) { + char *qualified_uri = NULL, *rp; struct stat st; - if(strcmp(uri, "") == 0) - return; - /* In case it's a file path. */ if(stat(uri, &st) == 0) { rp = realpath(uri, NULL); - u = g_strdup_printf("file://%s", rp); + qualified_uri = g_strdup_printf("file://%s", rp); free(rp); } else { - u = g_strrstr(uri, "://") ? g_strdup(uri) + qualified_uri = g_strrstr(uri, "://") ? g_strdup(uri) : g_strdup_printf("http://%s", uri); } - setatom(c, AtomUri, uri); + return qualified_uri; +} + +static void +loadgate(Client *c, const char *cross_from_origin, const char *cross_to_uri) { + char *cross_to_origin = origingeturi(cross_to_uri); + char *content = g_strdup_printf(originprompt, originpromptdigestsalt, originprompthmackey, cross_from_origin, cross_to_uri, cross_from_origin, cross_to_origin, cross_to_origin, cross_to_uri, cross_to_uri); + + origin_uri = g_strdup(cross_to_uri); + webkit_web_view_load_string(c->view, content, "text/html", NULL, cross_to_origin); + c->progress = 0; + c->title = g_strdup("Origin Crossing Gate"); + updatetitle(c); - /* prevents endless loop */ - if(strcmp(u, geturi(c)) == 0) { - reload(c, &a); + g_free(cross_to_origin); + g_free(content); +} + +static void +loaduri(Client *c, const Arg *arg, const enum NavTransparency navtrans) { + const char *uri = (char *)arg->v; + Arg a = { .b = FALSE }; + + if(strcmp(uri, "") == 0) + return; + + if (!sameoriginpolicy || !origin_uri || !originhas(origin_uri) || originmatch(uri, origin_uri)) { + setatom(c, AtomUri, uri); + + /* prevents endless loop */ + if(strcmp(uri, geturi(c)) == 0) { + reload(c, &a); + } else { + webkit_web_view_load_uri(c->view, uri); + c->progress = 0; + c->title = copystr(&c->title, uri); + updatetitle(c); + } } else { - webkit_web_view_load_uri(c->view, u); - c->progress = 0; - c->title = copystr(&c->title, u); - updatetitle(c); + newwindow(NULL, arg, 0, NavExplicit); } - g_free(u); } static void @@ -773,6 +846,9 @@ newclient(void) { "new-window-policy-decision-requested", G_CALLBACK(decidewindow), c); g_signal_connect(G_OBJECT(c->view), + "navigation-policy-decision-requested", + G_CALLBACK(decidenavigation), c); + g_signal_connect(G_OBJECT(c->view), "mime-type-policy-decision-requested", G_CALLBACK(decidedownload), c); g_signal_connect(G_OBJECT(c->view), @@ -921,11 +997,12 @@ newclient(void) { } static void -newwindow(Client *c, const Arg *arg, gboolean noembed) { +newwindow(Client *c, const Arg *arg, gboolean noembed, const enum NavTransparency navtrans) { guint i = 0; - const char *cmd[16], *uri; + const char *cmd[22], *uri; const Arg a = { .v = (void *)cmd }; char tmp[64]; + char *origin_packed = NULL; cmd[i++] = argv0; cmd[i++] = "-a"; @@ -949,8 +1026,20 @@ newwindow(Client *c, const Arg *arg, gboolean noembed) { cmd[i++] = "-s"; if(showxid) cmd[i++] = "-x"; + if(sameoriginpolicy) + cmd[i++] = "-O"; + cmd[i++] = originpromptfile; if(enablediskcache) cmd[i++] = "-D"; + if(navtrans == NavImplicit) { + cmd[i++] = "-R"; + if (originhas(origin_uri)) { + origin_packed = origingeturi(origin_uri); + } else { + origin_packed = g_strdup("-"); + } + cmd[i++] = origin_packed; + } cmd[i++] = "-c"; cmd[i++] = cookiefile; cmd[i++] = "--"; @@ -959,6 +1048,7 @@ newwindow(Client *c, const Arg *arg, gboolean noembed) { cmd[i++] = uri; cmd[i++] = NULL; spawn(NULL, &a); + g_free(origin_packed); } static gboolean @@ -1009,11 +1099,112 @@ menuactivate(GtkMenuItem *item, Client *c) { } } +static int +origincmp(const char *uri1, const char *uri2) { + /* Doesn't handle default ports, but otherwise should comply with RFC 6454, The Web Origin Concept. */ + int c; + if (g_str_has_prefix(uri1, "http://") && g_str_has_prefix(uri2, "http://")) { + return strncmp(uri1 + strlen("http://"), uri2 + strlen("http://"), strcspn(uri1 + strlen("http://"), "/?#")); + } else if (g_str_has_prefix(uri1, "https://") && g_str_has_prefix(uri2, "https://")) { + return strncmp(uri1 + strlen("https://"), uri2 + strlen("https://"), strcspn(uri1 + strlen("https://"), "/?#")); + } else { + c = strcmp(uri1, uri2); + if (c == 0) { + /* -1 when 0 to force a mismatch in originmatch() */ + c = -1; + } + return c; + } +} + +static int +originhas(const char *uri) { + char *origin = origingethost(uri); + int has = origin != NULL; + free(origin); + return has; +} + +static const char * +origingetproto(const char *uri) { + if (g_str_has_prefix(uri, "http://")) { + return "http"; + } else if (g_str_has_prefix(uri, "https://")) { + return "https"; + } else { + return NULL; + } +} + +/* caller must free() the return value, if not NULL */ +static char * +origingethost(const char *uri) { + /* Doesn't handle default ports, but otherwise should comply with RFC 6454, The Web Origin Concept. */ + char *origin = NULL; + size_t spansize; + size_t spanstart; + + if ( g_str_has_prefix(uri, "http://")) { + spanstart = strlen("http://"); + } else if (g_str_has_prefix(uri, "https://")) { + spanstart = strlen("https://"); + } else { + /* + * RFC 6454: this case should return a globally unique origin. + * + * As long as processes are per-origin + * (that is, new origins get a new process), + * then relying only on process state provides this uniqueness, + * since anything stored would be stored by an inaccessible key. + * + * So, when the caller gets this error, + * it should just bypass storage altogether. + */ + return NULL; + } + + spansize = strcspn(uri + spanstart, "/?#"); + if (spansize > 0 && uri[spanstart] == '.') { + /* kill attempt to traverse into parent folder */ + return NULL; + } + origin = malloc(sizeof(char) * (spansize + 1)); + if (origin) { + strncpy(origin, uri + spanstart, spansize); + origin[spansize] = '\0'; + /* malloc()'d origin */ + return origin; + } else { + /* ENOMEM set by malloc() */ + return NULL; + } +} + +/* caller must g_free() the return value, if not NULL */ +static char * +origingeturi(const char *uri) { + const char *origin_proto = origingetproto(uri); + char *origin_host = origingethost(uri); + char *origin_packed = NULL; + if (origin_host) { + origin_packed = g_strdup_printf("%s://%s", origin_proto, origin_host); + } + g_free(origin_host); + return origin_packed; +} + +static int +originmatch(const char *uri1, const char *uri2) { + return origincmp(uri1, uri2) == 0; +} + static void pasteuri(GtkClipboard *clipboard, const char *text, gpointer d) { - Arg arg = {.v = text }; + char *qualified_uri = qualify_uri(text); + Arg arg = {.v = qualified_uri }; if(text != NULL) - loaduri((Client *) d, &arg); + loaduri((Client *) d, &arg, NavExplicit); + g_free(qualified_uri); } static void @@ -1026,6 +1217,8 @@ processx(GdkXEvent *e, GdkEvent *event, gpointer d) { Client *c = (Client *)d; XPropertyEvent *ev; Arg arg; + const char *unqualified_uri = NULL; + char *qualified_uri = NULL; if(((XEvent *)e)->type == PropertyNotify) { ev = &((XEvent *)e)->xproperty; @@ -1036,10 +1229,17 @@ processx(GdkXEvent *e, GdkEvent *event, gpointer d) { return GDK_FILTER_REMOVE; } else if(ev->atom == atoms[AtomGo]) { - arg.v = getatom(c, AtomGo); - loaduri(c, &arg); - - return GDK_FILTER_REMOVE; + unqualified_uri = getatom(c, AtomGo); + if (unqualified_uri) { + qualified_uri = qualify_uri(unqualified_uri); + if (qualified_uri) { + arg.v = qualified_uri; + loaduri(c, &arg, NavExplicit); + g_free(qualified_uri); + + return GDK_FILTER_REMOVE; + } + } } } } @@ -1106,9 +1306,11 @@ setatom(Client *c, int a, const char *v) { } static void -setup(void) { +setup(const char *qualified_uri) { char *proxy; char *new_proxy; + char *origin; + char *originpath; SoupURI *puri; SoupSession *s; GError *error = NULL; @@ -1124,11 +1326,30 @@ setup(void) { atoms[AtomGo] = XInternAtom(dpy, "_SURF_GO", False); atoms[AtomUri] = XInternAtom(dpy, "_SURF_URI", False); + if (originpromptfile) + if (!g_file_get_contents(originpromptfile, &originprompt, NULL, NULL)) + die("Could not open origin prompt file\n"); + /* dirs and files */ - cookiefile = buildpath(cookiefile); + if (sameoriginpolicy && qualified_uri && originhas(qualified_uri)) { + origin = origingethost(qualified_uri); + + originpath = g_strdup_printf(origincookiefile, origin); + cookiefile = buildpath(originpath); + g_free(originpath); + + originpath = g_strdup_printf(origincachefolder, origin); + cachefolder = buildpath(originpath); + g_free(originpath); + + free(origin); + } else { + cookiefile = buildpath(cookiefile); + cachefolder = buildpath(cachefolder); + } + scriptfile = buildpath(scriptfile); stylefile = buildpath(stylefile); - cachefolder = buildpath(cachefolder); /* request handler */ s = webkit_get_default_session(); @@ -1332,6 +1553,8 @@ gettogglestat(Client *c){ togglestat[p++] = enablediskcache? 'D': 'd'; + togglestat[p++] = sameoriginpolicy? 'O' : 'o'; + g_object_get(G_OBJECT(settings), "auto-load-images", &value, NULL); togglestat[p++] = value? 'I': 'i'; @@ -1365,6 +1588,14 @@ getpagestat(Client *c) { static void updatetitle(Client *c) { char *t; + char *originstat; + + if(originhas(origin_uri)) { + originstat = origingethost(origin_uri); + } else { + originstat = g_strdup("-"); + } + if(showindicators) { gettogglestat(c); @@ -1374,11 +1605,14 @@ updatetitle(Client *c) { t = g_strdup_printf("%s:%s | %s", togglestat, pagestat, c->linkhover); } else if(c->progress != 100) { - t = g_strdup_printf("[%i%%] %s:%s | %s", c->progress, + t = g_strdup_printf("[%i%%] %s:%s | %s | %s", c->progress, togglestat, pagestat, + originstat, (c->title == NULL)? "" : c->title); } else { - t = g_strdup_printf("%s:%s | %s", togglestat, pagestat, + t = g_strdup_printf( "%s:%s | %s | %s", + togglestat, pagestat, + originstat, (c->title == NULL)? "" : c->title); } @@ -1388,6 +1622,8 @@ updatetitle(Client *c) { gtk_window_set_title(GTK_WINDOW(c->win), (c->title == NULL)? "" : c->title); } + + g_free(originstat); } static void @@ -1431,6 +1667,7 @@ int main(int argc, char *argv[]) { Arg arg; Client *c; + char *qualified_uri = NULL; memset(&arg, 0, sizeof(arg)); @@ -1487,6 +1724,13 @@ main(int argc, char *argv[]) { case 'N': enableinspector = 1; break; + case 'o': + sameoriginpolicy = 0; + break; + case 'O': + sameoriginpolicy = 1; + originpromptfile = EARGF(usage()); + break; case 'p': enableplugins = 0; break; @@ -1496,6 +1740,9 @@ main(int argc, char *argv[]) { case 'r': scriptfile = EARGF(usage()); break; + case 'R': + referring_origin = EARGF(usage()); + break; case 's': enablescripts = 0; break; @@ -1520,13 +1767,25 @@ main(int argc, char *argv[]) { default: usage(); } ARGEND; - if(argc > 0) - arg.v = argv[0]; + if(argc > 0) { + if (argv[0]) { + qualified_uri = qualify_uri(argv[0]); + } + } - setup(); + setup(qualified_uri); c = newclient(); - if(arg.v) { - loaduri(clients, &arg); + if(qualified_uri) { + if (originhas(qualified_uri)) { + origin_uri = qualified_uri; + } + if (sameoriginpolicy && referring_origin && (strcmp(referring_origin, "-") == 0 || !originmatch(referring_origin, qualified_uri))) { + loadgate(clients, referring_origin, qualified_uri); + } else { + arg.v = qualified_uri; + loaduri(clients, &arg, NavImplicit); + } + g_free(qualified_uri); } else { updatetitle(c); } @@ -1534,6 +1793,8 @@ main(int argc, char *argv[]) { gtk_main(); cleanup(); + g_free(qualified_uri); + return EXIT_SUCCESS; } -- 2.1.2