Skip to content

Commit 458f90c

Browse files
authored
Web security hardening (#4830)
* security hardening * fix some issues * rustfmt * add pr
1 parent 2035e05 commit 458f90c

24 files changed

+962
-131
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
1010
* fix: make sessions compatible across versions (https://github.com/zellij-org/zellij/pull/4439)
1111
* fix: occasional status-bar pop-out after resurrecting a session (https://github.com/zellij-org/zellij/pull/4440)
1212
* fix: properly serialize/resurrect layouts with one-line split panes (https://github.com/zellij-org/zellij/pull/4442)
13-
* feat: allow attaching to remote Zellij sessions over https (eg. `zellij attach https://example.com/my-cool-session`) (https://github.com/zellij-org/zellij/pull/4460)
13+
* feat: allow attaching to remote Zellij sessions over https (eg. `zellij attach https://example.com/my-cool-session`) (https://github.com/zellij-org/zellij/pull/4460 and https://github.com/zellij-org/zellij/pull/4830)
1414
* build: Update Rust toolchain to 1.90.0 (https://github.com/zellij-org/zellij/pull/4457)
1515
* feat: allow plugins to read pane scrollback (https://github.com/zellij-org/zellij/pull/4465)
1616
* infra: migrate wasm runtime from wasmtime to wasmi (https://github.com/zellij-org/zellij/pull/4449)

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/commands.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ pub(crate) fn create_auth_token(name: Option<String>, read_only: bool) -> Result
247247
create_token(name, read_only)
248248
.map(|(token, token_name)| {
249249
let access_type = if read_only { " (read-only)" } else { "" };
250-
format!("{}: {}{}", token, token_name, access_type)
250+
format!("{}: {}{}", token_name, token, access_type)
251251
})
252252
.map_err(|e| e.to_string())
253253
}
@@ -716,6 +716,8 @@ pub(crate) fn start_client(opts: CliArgs) {
716716
token: None,
717717
remember: false,
718718
forget: false,
719+
ca_cert: None,
720+
insecure: false,
719721
}));
720722
} else {
721723
opts.command = None;
@@ -748,6 +750,8 @@ pub(crate) fn start_client(opts: CliArgs) {
748750
token,
749751
remember,
750752
forget,
753+
ca_cert,
754+
insecure,
751755
})) = opts.command.clone()
752756
{
753757
if let Some(remote_session_url) = session_name.as_ref().and_then(|s| {
@@ -774,6 +778,8 @@ pub(crate) fn start_client(opts: CliArgs) {
774778
token,
775779
remember,
776780
forget,
781+
ca_cert,
782+
insecure,
777783
config_options.client_async_worker_tasks,
778784
) {
779785
eprintln!("{}", e);

zellij-client/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ tokio = { workspace = true }
4040
tokio-util = { workspace = true }
4141
isahc = { workspace = true }
4242
tokio-tungstenite = { version = "0.20", features = ["native-tls"] } #TODO: see about this native-tls...
43+
native-tls = "0.2"
4344
futures-util = "0.3"
4445
urlencoding = "2.1"
4546

zellij-client/assets/auth.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export async function getClientId(token, rememberMe, hasAuthenticationCookie) {
9696
export async function initAuthentication() {
9797
let token = null;
9898
let remember = null;
99-
let hasAuthenticationCookie = window.is_authenticated;
99+
let hasAuthenticationCookie = document.body.dataset.authenticated === "true";
100100

101101
if (!hasAuthenticationCookie) {
102102
const tokenResult = await waitForSecurityToken();

zellij-client/assets/index.html

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,8 @@
2020
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
2121
<title>Zellij Web Client</title>
2222
</head>
23-
<body>
23+
<body data-authenticated="IS_AUTHENTICATED">
2424
<div id="terminal" tabindex="0"></div>
25-
<script>
26-
window.is_authenticated = IS_AUTHENTICATED;
27-
</script>
2825
<script src="assets/modals.js"></script>
2926
<!-- Module files - order matters -->
3027
<script type="module" src="assets/utils.js"></script>

zellij-client/assets/modals.js

Lines changed: 87 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -411,19 +411,36 @@ function showErrorModal(title, description) {
411411
const modal = document.createElement('div');
412412
modal.className = 'security-modal error';
413413

414-
modal.innerHTML = `
415-
<div class="security-modal-content">
416-
<h3>${title}</h3>
417-
<div class="error-description">${description}</div>
418-
<div class="button-row">
419-
<button id="dismiss" class="dismiss-btn">Acknowledge</button>
420-
</div>
421-
<div class="status-bar"></div>
422-
</div>
423-
`;
414+
const content = document.createElement('div');
415+
content.className = 'security-modal-content';
416+
417+
const h3 = document.createElement('h3');
418+
h3.textContent = title;
419+
content.appendChild(h3);
420+
421+
const desc = document.createElement('div');
422+
desc.className = 'error-description';
423+
desc.textContent = description;
424+
content.appendChild(desc);
425+
426+
const buttonRow = document.createElement('div');
427+
buttonRow.className = 'button-row';
428+
429+
const dismissBtn = document.createElement('button');
430+
dismissBtn.id = 'dismiss';
431+
dismissBtn.className = 'dismiss-btn';
432+
dismissBtn.textContent = 'Acknowledge';
433+
buttonRow.appendChild(dismissBtn);
434+
content.appendChild(buttonRow);
435+
436+
const statusBar = document.createElement('div');
437+
statusBar.className = 'status-bar';
438+
content.appendChild(statusBar);
439+
440+
modal.appendChild(content);
424441

425442
document.body.appendChild(modal);
426-
modal.querySelector('#dismiss').focus();
443+
dismissBtn.focus();
427444

428445
const handleKeydown = (e) => {
429446
if (e.key === 'Enter' || e.key === 'Escape') {
@@ -440,7 +457,7 @@ function showErrorModal(title, description) {
440457
resolve();
441458
};
442459

443-
modal.querySelector('#dismiss').onclick = cleanup;
460+
dismissBtn.onclick = cleanup;
444461

445462
modal.onclick = (e) => {
446463
if (e.target === modal) {
@@ -459,25 +476,65 @@ function showReconnectionModal(attemptNumber, delaySeconds) {
459476
modal.style.background = 'rgba(28, 28, 28, 0.85)'; // More transparent to show terminal
460477

461478
const isFirstAttempt = attemptNumber === 1;
462-
const title = isFirstAttempt ? 'Connection Lost' : 'Reconnection Failed';
463-
const message = isFirstAttempt
464-
? `Reconnecting in <span id="countdown">${delaySeconds}</span> second${delaySeconds > 1 ? 's' : ''}...`
465-
: `Retrying in <span id="countdown">${delaySeconds}</span> second${delaySeconds > 1 ? 's' : ''}... (Attempt ${attemptNumber})`;
466-
467-
modal.innerHTML = `
468-
<div class="security-modal-content">
469-
<h3 id="modal-title">${title}</h3>
470-
<div class="error-description" id="modal-message">${message}</div>
471-
<div class="button-row" id="button-row">
472-
<button id="cancel" class="cancel-btn">Cancel</button>
473-
<button id="reconnect" class="submit-btn">Reconnect Now</button>
474-
</div>
475-
<div class="status-bar"></div>
476-
</div>
477-
`;
479+
const titleText = isFirstAttempt ? 'Connection Lost' : 'Reconnection Failed';
480+
481+
const contentDiv = document.createElement('div');
482+
contentDiv.className = 'security-modal-content';
483+
484+
const titleEl = document.createElement('h3');
485+
titleEl.id = 'modal-title';
486+
titleEl.textContent = titleText;
487+
contentDiv.appendChild(titleEl);
488+
489+
const messageEl = document.createElement('div');
490+
messageEl.className = 'error-description';
491+
messageEl.id = 'modal-message';
492+
493+
if (isFirstAttempt) {
494+
messageEl.appendChild(document.createTextNode('Reconnecting in '));
495+
const countdown = document.createElement('span');
496+
countdown.id = 'countdown';
497+
countdown.textContent = delaySeconds;
498+
messageEl.appendChild(countdown);
499+
messageEl.appendChild(document.createTextNode(delaySeconds > 1 ? ' seconds...' : ' second...'));
500+
} else {
501+
messageEl.appendChild(document.createTextNode('Retrying in '));
502+
const countdown = document.createElement('span');
503+
countdown.id = 'countdown';
504+
countdown.textContent = delaySeconds;
505+
messageEl.appendChild(countdown);
506+
messageEl.appendChild(document.createTextNode(
507+
(delaySeconds > 1 ? ' seconds' : ' second') + '... (Attempt ' + attemptNumber + ')'
508+
));
509+
}
510+
contentDiv.appendChild(messageEl);
511+
512+
const buttonRowEl = document.createElement('div');
513+
buttonRowEl.className = 'button-row';
514+
buttonRowEl.id = 'button-row';
515+
516+
const cancelBtn = document.createElement('button');
517+
cancelBtn.id = 'cancel';
518+
cancelBtn.className = 'cancel-btn';
519+
cancelBtn.textContent = 'Cancel';
520+
buttonRowEl.appendChild(cancelBtn);
521+
522+
const reconnectBtn = document.createElement('button');
523+
reconnectBtn.id = 'reconnect';
524+
reconnectBtn.className = 'submit-btn';
525+
reconnectBtn.textContent = 'Reconnect Now';
526+
buttonRowEl.appendChild(reconnectBtn);
527+
528+
contentDiv.appendChild(buttonRowEl);
529+
530+
const statusBarEl = document.createElement('div');
531+
statusBarEl.className = 'status-bar';
532+
contentDiv.appendChild(statusBarEl);
533+
534+
modal.appendChild(contentDiv);
478535

479536
document.body.appendChild(modal);
480-
modal.querySelector('#reconnect').focus();
537+
reconnectBtn.focus();
481538

482539
let countdownInterval;
483540
let remainingSeconds = delaySeconds;
@@ -503,7 +560,7 @@ function showReconnectionModal(attemptNumber, delaySeconds) {
503560
}
504561

505562
const messageElement = modal.querySelector('#modal-message');
506-
messageElement.innerHTML = 'Connecting...';
563+
messageElement.textContent = 'Connecting...';
507564
};
508565

509566
countdownInterval = setInterval(updateCountdown, 1000);

zellij-client/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,8 @@ pub fn start_remote_client(
535535
token: Option<String>,
536536
remember: bool,
537537
forget: bool,
538+
ca_cert: Option<std::path::PathBuf>,
539+
insecure: bool,
538540
async_worker_tasks: Option<usize>,
539541
) -> Result<Option<ConnectToSession>, RemoteClientError> {
540542
info!("Starting Zellij client!");
@@ -548,6 +550,8 @@ pub fn start_remote_client(
548550
token,
549551
remember,
550552
forget,
553+
ca_cert.as_deref(),
554+
insecure,
551555
)?;
552556

553557
let reconnect_to_session = None;

zellij-client/src/remote_attach/auth.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ pub async fn authenticate(
1919
server_base_url: &str,
2020
auth_token: &str,
2121
remember_me: bool,
22+
ca_cert: Option<&std::path::Path>,
23+
insecure: bool,
2224
) -> Result<(String, HttpClientWithCookies, Option<String>), RemoteClientError> {
23-
let http_client =
24-
HttpClientWithCookies::new().map_err(|e| RemoteClientError::Other(Box::new(e)))?;
25+
let http_client = HttpClientWithCookies::new(ca_cert, insecure)
26+
.map_err(|e| RemoteClientError::Other(Box::new(e)))?;
2527

2628
// Step 1: Login with auth token
2729
let login_url = format!("{}{}", server_base_url, LOGIN_ENDPOINT);
@@ -105,9 +107,11 @@ pub async fn authenticate(
105107
pub async fn validate_session_token(
106108
server_base_url: &str,
107109
session_token: &str,
110+
ca_cert: Option<&std::path::Path>,
111+
insecure: bool,
108112
) -> Result<(String, HttpClientWithCookies), RemoteClientError> {
109-
let http_client =
110-
HttpClientWithCookies::new().map_err(|e| RemoteClientError::Other(Box::new(e)))?;
113+
let http_client = HttpClientWithCookies::new(ca_cert, insecure)
114+
.map_err(|e| RemoteClientError::Other(Box::new(e)))?;
111115

112116
// Pre-populate the session_token cookie
113117
http_client.set_cookie("session_token".to_string(), session_token.to_string());

zellij-client/src/remote_attach/http_client.rs

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,30 @@ use super::config::connection_timeout;
22
use isahc::prelude::*;
33
use isahc::{config::RedirectPolicy, AsyncBody, HttpClient, Request, Response};
44
use std::collections::HashMap;
5+
use std::path::Path;
56
use std::sync::{Arc, Mutex};
67

7-
pub fn create_http_client() -> Result<HttpClient, isahc::Error> {
8-
HttpClient::builder()
8+
pub fn create_http_client(
9+
ca_cert: Option<&Path>,
10+
insecure: bool,
11+
) -> Result<HttpClient, isahc::Error> {
12+
let mut builder = HttpClient::builder()
913
.redirect_policy(RedirectPolicy::Follow)
10-
.ssl_options(isahc::config::SslOption::DANGER_ACCEPT_INVALID_CERTS)
11-
.timeout(connection_timeout())
12-
.build()
14+
.timeout(connection_timeout());
15+
16+
if insecure {
17+
eprintln!(
18+
"WARNING: TLS certificate validation is disabled. This connection is NOT secure."
19+
);
20+
builder = builder.ssl_options(
21+
isahc::config::SslOption::DANGER_ACCEPT_INVALID_CERTS
22+
| isahc::config::SslOption::DANGER_ACCEPT_INVALID_HOSTS,
23+
);
24+
} else if let Some(ca_path) = ca_cert {
25+
builder = builder.ssl_ca_certificate(isahc::config::CaCertificate::file(ca_path));
26+
}
27+
28+
builder.build()
1329
}
1430

1531
pub struct HttpClientWithCookies {
@@ -18,9 +34,9 @@ pub struct HttpClientWithCookies {
1834
}
1935

2036
impl HttpClientWithCookies {
21-
pub fn new() -> Result<Self, isahc::Error> {
37+
pub fn new(ca_cert: Option<&Path>, insecure: bool) -> Result<Self, isahc::Error> {
2238
Ok(Self {
23-
client: create_http_client()?,
39+
client: create_http_client(ca_cert, insecure)?,
2440
cookies: Arc::new(Mutex::new(HashMap::new())),
2541
})
2642
}

0 commit comments

Comments
 (0)