fix: Electron security issues and TOTP/None auth issues
This commit is contained in:
@@ -57,10 +57,6 @@ app.use(
|
||||
"http://127.0.0.1:3000",
|
||||
];
|
||||
|
||||
if (allowedOrigins.includes(origin)) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
if (origin.startsWith("https://")) {
|
||||
return callback(null, true);
|
||||
}
|
||||
@@ -69,6 +65,10 @@ app.use(
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
if (allowedOrigins.includes(origin)) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
callback(new Error("Not allowed by CORS"));
|
||||
},
|
||||
credentials: true,
|
||||
@@ -172,6 +172,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
||||
keyPassword,
|
||||
authType,
|
||||
credentialId,
|
||||
userProvidedPassword,
|
||||
} = req.body;
|
||||
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
@@ -264,44 +265,31 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
||||
keepaliveCountMax: 3,
|
||||
algorithms: {
|
||||
kex: [
|
||||
"curve25519-sha256",
|
||||
"curve25519-sha256@libssh.org",
|
||||
"ecdh-sha2-nistp521",
|
||||
"ecdh-sha2-nistp384",
|
||||
"ecdh-sha2-nistp256",
|
||||
"diffie-hellman-group-exchange-sha256",
|
||||
"diffie-hellman-group14-sha256",
|
||||
"diffie-hellman-group14-sha1",
|
||||
"diffie-hellman-group-exchange-sha1",
|
||||
"diffie-hellman-group1-sha1",
|
||||
],
|
||||
serverHostKey: [
|
||||
"ssh-ed25519",
|
||||
"ecdsa-sha2-nistp521",
|
||||
"ecdsa-sha2-nistp384",
|
||||
"ecdsa-sha2-nistp256",
|
||||
"rsa-sha2-512",
|
||||
"rsa-sha2-256",
|
||||
"ssh-rsa",
|
||||
"ssh-dss",
|
||||
"diffie-hellman-group-exchange-sha256",
|
||||
"diffie-hellman-group-exchange-sha1",
|
||||
"ecdh-sha2-nistp256",
|
||||
"ecdh-sha2-nistp384",
|
||||
"ecdh-sha2-nistp521",
|
||||
],
|
||||
cipher: [
|
||||
"chacha20-poly1305@openssh.com",
|
||||
"aes256-gcm@openssh.com",
|
||||
"aes128-gcm@openssh.com",
|
||||
"aes256-ctr",
|
||||
"aes192-ctr",
|
||||
"aes128-ctr",
|
||||
"aes256-cbc",
|
||||
"aes192-cbc",
|
||||
"aes192-ctr",
|
||||
"aes256-ctr",
|
||||
"aes128-gcm@openssh.com",
|
||||
"aes256-gcm@openssh.com",
|
||||
"aes128-cbc",
|
||||
"aes192-cbc",
|
||||
"aes256-cbc",
|
||||
"3des-cbc",
|
||||
],
|
||||
hmac: [
|
||||
"hmac-sha2-512-etm@openssh.com",
|
||||
"hmac-sha2-256-etm@openssh.com",
|
||||
"hmac-sha2-512",
|
||||
"hmac-sha2-512-etm@openssh.com",
|
||||
"hmac-sha2-256",
|
||||
"hmac-sha2-512",
|
||||
"hmac-sha1",
|
||||
"hmac-md5",
|
||||
],
|
||||
@@ -309,8 +297,6 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
||||
},
|
||||
};
|
||||
|
||||
let authMethodNotAvailable = false;
|
||||
|
||||
if (
|
||||
resolvedCredentials.authType === "key" &&
|
||||
resolvedCredentials.sshKey &&
|
||||
@@ -348,33 +334,11 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
||||
.status(400)
|
||||
.json({ error: "Password required for password authentication" });
|
||||
}
|
||||
config.password = resolvedCredentials.password;
|
||||
|
||||
if (userProvidedPassword) {
|
||||
config.password = resolvedCredentials.password;
|
||||
}
|
||||
} else if (resolvedCredentials.authType === "none") {
|
||||
config.authHandler = (
|
||||
methodsLeft: string[] | null,
|
||||
partialSuccess: boolean,
|
||||
callback: (nextMethod: string | false) => void,
|
||||
) => {
|
||||
if (methodsLeft && methodsLeft.length > 0) {
|
||||
if (methodsLeft.includes("keyboard-interactive")) {
|
||||
callback("keyboard-interactive");
|
||||
} else {
|
||||
authMethodNotAvailable = true;
|
||||
fileLogger.error(
|
||||
"Server does not support keyboard-interactive auth",
|
||||
{
|
||||
operation: "ssh_auth_handler_no_keyboard",
|
||||
hostId,
|
||||
sessionId,
|
||||
methodsAvailable: methodsLeft,
|
||||
},
|
||||
);
|
||||
callback(false);
|
||||
}
|
||||
} else {
|
||||
callback(false);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
fileLogger.warn(
|
||||
"No valid authentication method provided for file manager",
|
||||
@@ -451,36 +415,26 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
||||
client.on("error", (err) => {
|
||||
if (responseSent) return;
|
||||
responseSent = true;
|
||||
fileLogger.error("SSH connection failed for file manager", {
|
||||
operation: "file_connect",
|
||||
sessionId,
|
||||
hostId,
|
||||
ip,
|
||||
port,
|
||||
username,
|
||||
error: err.message,
|
||||
});
|
||||
|
||||
if (authMethodNotAvailable && resolvedCredentials.authType === "none") {
|
||||
res.status(200).json({
|
||||
status: "auth_required",
|
||||
message:
|
||||
"The server does not support keyboard-interactive authentication. Please provide credentials.",
|
||||
reason: "no_keyboard",
|
||||
});
|
||||
} else if (
|
||||
if (
|
||||
resolvedCredentials.authType === "none" &&
|
||||
(err.message.includes("All configured authentication methods failed") ||
|
||||
err.message.includes("No supported authentication methods available") ||
|
||||
err.message.includes("authentication methods failed"))
|
||||
(err.message.includes("authentication") ||
|
||||
err.message.includes("All configured authentication methods failed"))
|
||||
) {
|
||||
res.status(200).json({
|
||||
res.json({
|
||||
status: "auth_required",
|
||||
message:
|
||||
"The server does not support keyboard-interactive authentication. Please provide credentials.",
|
||||
reason: "no_keyboard",
|
||||
});
|
||||
} else {
|
||||
fileLogger.error("SSH connection failed for file manager", {
|
||||
operation: "file_connect",
|
||||
sessionId,
|
||||
hostId,
|
||||
ip,
|
||||
port,
|
||||
username,
|
||||
error: err.message,
|
||||
});
|
||||
res.status(500).json({ status: "error", message: err.message });
|
||||
}
|
||||
});
|
||||
@@ -564,13 +518,43 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
||||
/password/i.test(p.prompt),
|
||||
);
|
||||
|
||||
if (
|
||||
resolvedCredentials.authType === "none" &&
|
||||
passwordPromptIndex !== -1
|
||||
) {
|
||||
if (responseSent) return;
|
||||
responseSent = true;
|
||||
|
||||
client.end();
|
||||
|
||||
res.json({
|
||||
status: "auth_required",
|
||||
reason: "no_keyboard",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasStoredPassword && passwordPromptIndex !== -1) {
|
||||
if (responseSent) {
|
||||
const responses = prompts.map((p) => {
|
||||
if (/password/i.test(p.prompt) && resolvedCredentials.password) {
|
||||
return resolvedCredentials.password;
|
||||
}
|
||||
return "";
|
||||
});
|
||||
finish(responses);
|
||||
return;
|
||||
}
|
||||
responseSent = true;
|
||||
|
||||
if (pendingTOTPSessions[sessionId]) {
|
||||
const responses = prompts.map((p) => {
|
||||
if (/password/i.test(p.prompt) && resolvedCredentials.password) {
|
||||
return resolvedCredentials.password;
|
||||
}
|
||||
return "";
|
||||
});
|
||||
finish(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2446,6 +2430,15 @@ app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => {
|
||||
: code;
|
||||
const cleanOutput = output.replace(/EXIT_CODE:\d+$/, "").trim();
|
||||
|
||||
fileLogger.info("File execution completed", {
|
||||
operation: "execute_file",
|
||||
sessionId,
|
||||
filePath,
|
||||
exitCode: actualExitCode,
|
||||
outputLength: cleanOutput.length,
|
||||
errorLength: errorOutput.length,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
exitCode: actualExitCode,
|
||||
|
||||
@@ -64,47 +64,21 @@ const wss = new WebSocketServer({
|
||||
const token = url.query.token as string;
|
||||
|
||||
if (!token) {
|
||||
sshLogger.warn("WebSocket connection rejected: missing token", {
|
||||
operation: "websocket_auth_reject",
|
||||
reason: "missing_token",
|
||||
ip: info.req.socket.remoteAddress,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const payload = await authManager.verifyJWTToken(token);
|
||||
|
||||
if (!payload) {
|
||||
sshLogger.warn("WebSocket connection rejected: invalid token", {
|
||||
operation: "websocket_auth_reject",
|
||||
reason: "invalid_token",
|
||||
ip: info.req.socket.remoteAddress,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (payload.pendingTOTP) {
|
||||
sshLogger.warn(
|
||||
"WebSocket connection rejected: TOTP verification pending",
|
||||
{
|
||||
operation: "websocket_auth_reject",
|
||||
reason: "totp_pending",
|
||||
userId: payload.userId,
|
||||
ip: info.req.socket.remoteAddress,
|
||||
},
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const existingConnections = userConnections.get(payload.userId);
|
||||
if (existingConnections && existingConnections.size >= 3) {
|
||||
sshLogger.warn("WebSocket connection rejected: too many connections", {
|
||||
operation: "websocket_auth_reject",
|
||||
reason: "connection_limit",
|
||||
userId: payload.userId,
|
||||
currentConnections: existingConnections.size,
|
||||
ip: info.req.socket.remoteAddress,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -127,28 +101,12 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
const token = url.query.token as string;
|
||||
|
||||
if (!token) {
|
||||
sshLogger.warn(
|
||||
"WebSocket connection rejected: missing token in connection",
|
||||
{
|
||||
operation: "websocket_connection_reject",
|
||||
reason: "missing_token",
|
||||
ip: req.socket.remoteAddress,
|
||||
},
|
||||
);
|
||||
ws.close(1008, "Authentication required");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = await authManager.verifyJWTToken(token);
|
||||
if (!payload) {
|
||||
sshLogger.warn(
|
||||
"WebSocket connection rejected: invalid token in connection",
|
||||
{
|
||||
operation: "websocket_connection_reject",
|
||||
reason: "invalid_token",
|
||||
ip: req.socket.remoteAddress,
|
||||
},
|
||||
);
|
||||
ws.close(1008, "Authentication required");
|
||||
return;
|
||||
}
|
||||
@@ -169,11 +127,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
|
||||
const dataKey = userCrypto.getUserDataKey(userId);
|
||||
if (!dataKey) {
|
||||
sshLogger.warn("WebSocket connection rejected: data locked", {
|
||||
operation: "websocket_data_locked",
|
||||
userId,
|
||||
ip: req.socket.remoteAddress,
|
||||
});
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
@@ -213,11 +166,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
ws.on("message", (msg: RawData) => {
|
||||
const currentDataKey = userCrypto.getUserDataKey(userId);
|
||||
if (!currentDataKey) {
|
||||
sshLogger.warn("WebSocket message rejected: data access expired", {
|
||||
operation: "websocket_message_rejected",
|
||||
userId,
|
||||
reason: "data_access_expired",
|
||||
});
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
@@ -371,6 +319,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
if (credentialsData.password) {
|
||||
credentialsData.hostConfig.password = credentialsData.password;
|
||||
credentialsData.hostConfig.authType = "password";
|
||||
(credentialsData.hostConfig as any).userProvidedPassword = true;
|
||||
} else if (credentialsData.sshKey) {
|
||||
credentialsData.hostConfig.key = credentialsData.sshKey;
|
||||
credentialsData.hostConfig.keyPassword = credentialsData.keyPassword;
|
||||
@@ -776,13 +725,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
|
||||
sshConn.on("close", () => {
|
||||
clearTimeout(connectionTimeout);
|
||||
sshLogger.warn("SSH connection closed by server", {
|
||||
operation: "ssh_close",
|
||||
hostId: id,
|
||||
ip,
|
||||
port,
|
||||
hadStream: !!sshStream,
|
||||
});
|
||||
cleanupSSH(connectionTimeout);
|
||||
});
|
||||
|
||||
@@ -795,15 +737,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
prompts: Array<{ prompt: string; echo: boolean }>,
|
||||
finish: (responses: string[]) => void,
|
||||
) => {
|
||||
if (resolvedCredentials.authType === "none") {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "keyboard_interactive_available",
|
||||
message: "Keyboard-interactive authentication is available",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const promptTexts = prompts.map((p) => p.prompt);
|
||||
const totpPromptIndex = prompts.findIndex((p) =>
|
||||
/verification code|verification_code|token|otp|2fa|authenticator|google.*auth/i.test(
|
||||
@@ -854,7 +787,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
);
|
||||
|
||||
if (!hasStoredPassword && passwordPromptIndex !== -1) {
|
||||
if (keyboardInteractiveResponded && totpPromptSent) {
|
||||
if (keyboardInteractiveResponded) {
|
||||
return;
|
||||
}
|
||||
keyboardInteractiveResponded = true;
|
||||
@@ -898,7 +831,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
host: ip,
|
||||
port,
|
||||
username,
|
||||
tryKeyboard: resolvedCredentials.authType === "none",
|
||||
tryKeyboard: true,
|
||||
keepaliveInterval: 30000,
|
||||
keepaliveCountMax: 3,
|
||||
readyTimeout: 60000,
|
||||
@@ -964,22 +897,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
};
|
||||
|
||||
if (resolvedCredentials.authType === "none") {
|
||||
connectConfig.authHandler = (
|
||||
methodsLeft: string[] | null,
|
||||
partialSuccess: boolean,
|
||||
callback: (nextMethod: string | false) => void,
|
||||
) => {
|
||||
if (methodsLeft && methodsLeft.length > 0) {
|
||||
if (methodsLeft.includes("keyboard-interactive")) {
|
||||
callback("keyboard-interactive");
|
||||
} else {
|
||||
authMethodNotAvailable = true;
|
||||
callback(false);
|
||||
}
|
||||
} else {
|
||||
callback(false);
|
||||
}
|
||||
};
|
||||
} else if (resolvedCredentials.authType === "password") {
|
||||
if (!resolvedCredentials.password) {
|
||||
sshLogger.error(
|
||||
@@ -994,7 +911,10 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
);
|
||||
return;
|
||||
}
|
||||
connectConfig.password = resolvedCredentials.password;
|
||||
|
||||
if ((hostConfig as any).userProvidedPassword) {
|
||||
connectConfig.password = resolvedCredentials.password;
|
||||
}
|
||||
} else if (
|
||||
resolvedCredentials.authType === "key" &&
|
||||
resolvedCredentials.key
|
||||
|
||||
Reference in New Issue
Block a user