Skip to content

Commit

Permalink
login: Prevent multiple logins in a single browser session
Browse files Browse the repository at this point in the history
unless allowed by cockpit.conf.

If the login page is loaded and a valid session cookie is already
available, then we are about to log into a second host from the same
browser session, and both logins will have access to each others
cookies. This should only be allowed when AllowMultiHost is true. If
it is not true, the login page immediately redirects to the session
for the existing cookie.

Information about session cookies is not available to login.js, so
cockpit-ws helps out by exposing which ones are present, without
exposing the cookies themselves.
  • Loading branch information
mvollmer committed Aug 13, 2024
1 parent 0a74fdb commit 1dd4eca
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 25 deletions.
23 changes: 23 additions & 0 deletions pkg/static/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,27 @@ import "./login.scss";
event.stopPropagation();
}

function deal_with_multihost() {
// If we are currently logged in to some machine, but still
// end up on the login page, we are about to load resources
// from two machines into the same browser origin. This needs
// to be allowed explicitly via a configuration setting.

const logged_into = environment.logged_into || [];
const cur_machine = logged_into.length > 0 ? logged_into[0] : null;

function redirect_to_current_machine() {
if (cur_machine === ".") {
login_reload("/");
} else {
login_reload("/=" + cur_machine);
}
}

if (cur_machine && !environment.page.allow_multihost)
redirect_to_current_machine();
}

function boot() {
window.onload = null;

Expand All @@ -348,6 +369,8 @@ import "./login.scss";
document.documentElement.dir = window.cockpit_po[""]["language-direction"];
}

deal_with_multihost();

setup_path_globals(window.location.pathname);

/* Determine if we are nested or not, and switch styles */
Expand Down
57 changes: 39 additions & 18 deletions src/ws/cockpitauth.c
Original file line number Diff line number Diff line change
Expand Up @@ -1241,13 +1241,38 @@ base64_decode_string (const char *enc)
return dec;
}

static CockpitSession *
session_for_cookie_value (CockpitAuth *self,
const gchar *cookie_value)
{
gchar *decoded = NULL;
const char *prefix = "v=2;k=";
CockpitSession *ret = NULL;

g_return_val_if_fail (self != NULL, FALSE);

if (cookie_value)
{
decoded = base64_decode_string (cookie_value);
if (decoded != NULL)
{
if (g_str_has_prefix (decoded, prefix))
ret = g_hash_table_lookup (self->sessions, decoded);
else
g_debug ("invalid or unsupported cookie: %s", decoded);

g_free (decoded);
}
}

return ret;
}

static CockpitSession *
session_for_request (CockpitAuth *self,
CockpitWebRequest *request)
{
gchar *cookie = NULL;
gchar *raw = NULL;
const char *prefix = "v=2;k=";
CockpitSession *ret = NULL;
gchar *application;
gchar *cookie_name = NULL;
Expand All @@ -1260,23 +1285,12 @@ session_for_request (CockpitAuth *self,

cookie_name = application_cookie_name (application);
raw = cockpit_web_request_parse_cookie (request, cookie_name);
if (raw)
{
cookie = base64_decode_string (raw);
if (cookie != NULL)
{
if (g_str_has_prefix (cookie, prefix))
ret = g_hash_table_lookup (self->sessions, cookie);
else
g_debug ("invalid or unsupported cookie: %s", cookie);
ret = session_for_cookie_value (self, raw);

/* We must never find the default session based on a cookie */
g_assert (!ret || !g_str_equal (ret->cookie, LOCAL_SESSION));
g_assert (!ret || !g_str_equal (ret->name, LOCAL_SESSION));
g_free (cookie);
}
g_free (raw);
}
/* We must never find the default session based on a cookie */
g_assert (!ret || !g_str_equal (ret->cookie, LOCAL_SESSION));
g_assert (!ret || !g_str_equal (ret->name, LOCAL_SESSION));
g_free (raw);

/* Check for a default session for auto-login */
if (!ret)
Expand Down Expand Up @@ -1309,6 +1323,13 @@ cockpit_auth_check_cookie (CockpitAuth *self,
}
}

gboolean
cockpit_auth_is_valid_cookie_value (CockpitAuth *self,
const gchar *cookie_value)
{
return session_for_cookie_value (self, cookie_value) != NULL;
}

void
cockpit_auth_local_async (CockpitAuth *self,
const gchar *user,
Expand Down
5 changes: 4 additions & 1 deletion src/ws/cockpitauth.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@ gboolean cockpit_auth_local_finish (CockpitAuth *self,
CockpitWebService * cockpit_auth_check_cookie (CockpitAuth *self,
CockpitWebRequest *request);

gchar * cockpit_auth_parse_application (const gchar *path,
gboolean cockpit_auth_is_valid_cookie_value (CockpitAuth *self,
const gchar *cookie_value);

gchar * cockpit_auth_parse_application (const gchar *path,
gboolean *is_host);

gchar * cockpit_auth_empty_cookie_value (const gchar *path,
Expand Down
45 changes: 43 additions & 2 deletions src/ws/cockpithandlers.c
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,47 @@ add_page_to_environment (JsonObject *object,
json_object_set_object_member (object, "page", page);
}

static void
add_logged_into_to_environment (JsonObject *object,
CockpitAuth *auth,
GHashTable *request_headers)
{
JsonArray *logged_into = json_array_new ();

gchar *h = g_hash_table_lookup (request_headers, "Cookie");
while (h && *h) {
const gchar *start = h;
while (*h && *h != '=')
h++;
const gchar *equal = h;
while (*h && *h != ';')
h++;
const gchar *end = h;
if (*h)
h++;
while (*h && *h == ' ')
h++;

if (*equal != '=')
continue;

g_autofree gchar *value = g_strndup (equal + 1, end - equal - 1);

if (!cockpit_auth_is_valid_cookie_value (auth, value))
continue;

g_autofree gchar *name = g_strndup (start, equal - start);
if (g_str_equal (name, "cockpit"))
json_array_add_string_element(logged_into, ".");
else if (g_str_has_prefix (name, "machine-cockpit+"))
json_array_add_string_element(logged_into, name + strlen("machine-cockpit+"));
}

json_object_set_array_member (object, "logged_into", logged_into);
}

static GBytes *
build_environment (GHashTable *os_release)
build_environment (GHashTable *os_release, CockpitAuth *auth, GHashTable *request_headers)
{
/*
* We don't include entirety of os-release into the
Expand Down Expand Up @@ -310,6 +349,7 @@ build_environment (GHashTable *os_release)
json_object_set_boolean_member (object, "is_cockpit_client", is_cockpit_client);

add_page_to_environment (object, is_cockpit_client);
add_logged_into_to_environment (object, auth, request_headers);

hostname = g_malloc0 (HOST_NAME_MAX + 1);
gethostname (hostname, HOST_NAME_MAX);
Expand Down Expand Up @@ -386,7 +426,7 @@ send_login_html (CockpitWebResponse *response,
GBytes *po_bytes;
CockpitWebFilter *filter3 = NULL;

environment = build_environment (ws->os_release);
environment = build_environment (ws->os_release, ws->auth, headers);
filter = cockpit_web_inject_new (marker, environment, 1);
g_bytes_unref (environment);
cockpit_web_response_add_filter (response, filter);
Expand Down Expand Up @@ -455,6 +495,7 @@ send_login_html (CockpitWebResponse *response,
"Content-Security-Policy", content_security_policy,
"Set-Cookie", cookie_line,
NULL);
cockpit_web_response_set_cache_type (response, COCKPIT_WEB_RESPONSE_NO_CACHE);
if (cockpit_web_response_queue (response, bytes))
cockpit_web_response_complete (response);

Expand Down
22 changes: 18 additions & 4 deletions test/verify/check-embed
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import testlib
@testlib.nondestructive
class TestEmbed(testlib.MachineCase):

def testBasic(self):
def testBasic(self, allow_multi_host=True):
b = self.browser
m = self.machine

Expand All @@ -39,9 +39,10 @@ class TestEmbed(testlib.MachineCase):

# replace the shell with our embedded page, this way we can avoid
# cross-origin errors when executing js in the iframe
m.write("/etc/cockpit/cockpit.conf", """
m.write("/etc/cockpit/cockpit.conf", f"""
[WebService]
Shell=/embed-cockpit/index.html
AllowMultiHost={"yes" if allow_multi_host else "no"}
""")
m.start_cockpit()
self.login_and_go()
Expand All @@ -61,12 +62,25 @@ Shell=/embed-cockpit/index.html
b.switch_to_frame("embed-terminal")
b.wait_visible("#terminal")

# Clicking on the link with separate auth, shouldn't log in automatically
# Clicking on the link with separate auth, what happens
# depends on allow_multi_host
b.switch_to_top()
b.click("#embed-auth")
b.wait_visible("iframe[name='embed-auth'][loaded]")
b.switch_to_frame("embed-auth")
b.wait_visible("#login-user-input")
if allow_multi_host:
# When multiple connections are allowed, we get a fresh
# login page
b.wait_visible("#login-user-input")
else:
# When multiple connections are not allowed, we get
# redirected to "/" of the already open session. This
# loads the shell, which is /embed-cockpit/index.html in
# this test...
b.wait_visible("#embed-links")

def testNoMultiHost(self):
self.testBasic(allow_multi_host=False)

@testlib.skipBrowser("Chromium cannot inspect cross-origin frames", "chromium")
def testCrossOrigin(self):
Expand Down

0 comments on commit 1dd4eca

Please sign in to comment.