CVE-2021-3660 (C, published 2022-03-10T17:42Z)
Description: value: Cockpit (and its plugins) do not seem to protect itself against clickjacking. It is possible to render a page from a cockpit server via another website, inside an <iFrame> HTML entry. This may be used by a malicious website in clickjacking or similar attacks.
Severity: MEDIUM (CVSS v3 base score: 4.3)
Weakness: CWE-1021 \u2014 Improper Restriction of Rendered UI Layers or Frames
The web application does not restrict or incorrectly restricts frame objects or UI layers that belong to another application or domain, which can lead to user confusion about which interface the user is interacting with.
Repository: https://github.com/cockpit-project/cockpit
Files changed:
doc/guide/embedding.xml
pkg/base1/test-http.js
src/bridge/test-httpstream.c
src/bridge/test-packages.c
src/common/cockpitwebresponse.c
src/common/test-webresponse.c
src/ws/test-channelresponse.c
test/verify/check-connection
test/verify/check-embed
Commit message: common: Restrict frame embedding to same origin

Declare `X-Frame-Options: sameorigin` [1] so that cockpit frames can
only be embedded into pages coming from the same origin. This is similar
to setting CORP in commit 2b38b8de92f9a (which applies to `<script>`,
`<img>`, etc.).

The main use case for embedding is to run cockpit-ws behind a reverse
proxy, while also serving other pages. Cross-origin embedding is
discouraged these days to prevent "clickjacking".

Cross-origin embedding already did not work in most cases: Frames would
always just show the login page.  However, this looks confusing and is
unclean. With X-Frame-Options, the browser instead shows an explanatory
error page.

Mention the same origin requirement in the embedding documentation.

Fixes #16122
https://bugzilla.redhat.com/show_bug.cgi?id=1980688
CVE-2021-3660

[1] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options


Vulnerable code:
    Cockpit components can be integrated.</para>
#define STATIC_HEADERS "\"Cross-Origin-Resource-Policy\":\"same-origin\",\"Referrer-Policy\":\"no-referrer\",\"X-Content-Type-Options\":\"nosniff\",\"X-DNS-Prefetch-Control\":\"off\""
#define STATIC_HEADERS "\"X-DNS-Prefetch-Control\":\"off\",\"Referrer-Policy\":\"no-referrer\",\"X-Content-Type-Options\":\"nosniff\",\"Cross-Origin-Resource-Policy\": \"same-origin\""
#define STATIC_HEADERS "X-DNS-Prefetch-Control: off\r\nReferrer-Policy: no-referrer\r\nX-Content-Type-Options: nosniff\r\nCross-Origin-Resource-Policy: same-origin\r\n\r\n"
#define STATIC_HEADERS "X-Content-Type-Options: nosniff\r\nX-DNS-Prefetch-Control: off\r\nReferrer-Policy: no-referrer\r\nCross-Origin-Resource-Policy: same-origin\r\n"
        # CORP is also set for dynamic paths
        # FIXME (#16122): we should not even get that far, frame loading should already be blocked here
        # second line of defense: existing login cookie does not work (default browser protection)
        b.wait_visible("#login")
        b.set_val("#login-user-input", "admin")
        b.set_val("#login-password-input", "foobar")
        b.click('#login-button')
        b.expect_load_frame("embed-full")

        # third line of defense: login succeeds and creates a PAM session; but loading session UI does not
        # (again, due to default browser protection)
        b.wait_visible("#login")

Fixed code:
    Cockpit components can be integrated. Due to frame security policy restrictions,
    this only works if Cockpit and the web application have the <emphasis>same origin</emphasis>;
    this is commonly achieved by running both from a common reverse proxy.</para>
                    "X-Frame-Options": "sameorigin",
                    "X-Frame-Options": "sameorigin",
#define STATIC_HEADERS "\"Cross-Origin-Resource-Policy\":\"same-origin\",\"Referrer-Policy\":\"no-referrer\",\"X-Content-Type-Options\":\"nosniff\",\"X-DNS-Prefetch-Control\":\"off\",\"X-Frame-Options\":\"sameorigin\""
#define STATIC_HEADERS "\"X-DNS-Prefetch-Control\":\"off\",\"Referrer-Policy\":\"no-referrer\",\"X-Content-Type-Options\":\"nosniff\",\"Cross-Origin-Resource-Policy\": \"same-origin\",\"X-Frame-Options\": \"sameorigin\""
    HEADER_X_FRAME_OPTIONS = 1 << 8,
  if (g_ascii_strcasecmp ("X-Frame-Options", name) == 0)
    return HEADER_X_FRAME_OPTIONS;
  /* This is the counterpart for iframe embedding, line of defence against clickjacking */
  if ((seen & HEADER_X_FRAME_OPTIONS) == 0)
    g_string_append (string, "X-Frame-Options: sameorigin\r\n");
#define STATIC_HEADERS "X-DNS-Prefetch-Control: off\r\nReferrer-Policy: no-referrer\r\nX-Content-Type-Options: nosniff\r\nCross-Origin-Resource-Policy: same-origin\r\nX-Frame-Options: sameorigin\r\n\r\n"
#define STATIC_HEADERS "X-Content-Type-Options: nosniff\r\nX-DNS-Prefetch-Control: off\r\nReferrer-Policy: no-referrer\r\nCross-Origin-Resource-Policy: same-origin\r\nX-Frame-Options: sameorigin\r\n"
        # CORP and Frame-Options are also set for dynamic paths
        self.assertIn("X-Frame-Options: sameorigin", headers)
        self.assertIn("X-Frame-Options: sameorigin\r\n", headers)
        # X-Frame-Options sameorigin blocks frame
        if b.cdp.browser == "firefox":
            b.wait_visible("body.neterror")
        self.assertFalse(b.is_present("#login"))