Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

login: Prevent multiple logins in a single browser session #20860

Merged
merged 2 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 || [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test.

const cur_machine = logged_into.length > 0 ? logged_into[0] : null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const cur_machine = environment.logged_into?.[0]

(but if you prefer this, it's fine)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks too much like Perl for my taste... :-) but let's do it.


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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test.

}
}

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use g_autofree here, drop the declaration in line 1248 and the g_free below.

update: Oh, this is moved code. OK to keep if you prefer not to touch it.

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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please outdent again

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);
Comment on lines +287 to +309
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, do you want sscanf() here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, that would be the second time in my life that I use sscanf... hmm.

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
26 changes: 18 additions & 8 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,19 +39,16 @@ 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()

b.wait_visible("#embed-loaded")
b.wait_visible("#embed-address")
m.write("/etc/cockpit/cockpit.conf", """
[WebService]
Shell=/shell/index.html
""")
b.set_val("#embed-address", f"http://{m.web_address}:{m.web_port}")
b.click("#embed-full")
b.wait_visible("iframe[name='embed-full'][loaded]")
Expand All @@ -65,12 +62,25 @@ Shell=/shell/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