From c4c49b6d281bb76421620aa0f075a634432ed9ee Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Sun, 18 Oct 2026 21:54:50 +0100 Subject: [PATCH] Desktop, Cli: Bind OneDrive OAuth and resource servers to loopback and validate OAuth state --- packages/app-cli/app/ResourceServer.ts | 2 +- packages/lib/onedrive-api-node-utils.ts | 7 +++++-- packages/lib/onedrive-api.ts | 5 +++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/app-cli/app/ResourceServer.ts b/packages/app-cli/app/ResourceServer.ts index ac5e9fdcf1..cb2e430a31 100644 --- a/packages/app-cli/app/ResourceServer.ts +++ b/packages/app-cli/app/ResourceServer.ts @@ -84,7 +84,7 @@ class ResourceServer { this.logger().error('Resource server:', error); }); - this.server_.listen(this.port_); + this.server_.listen(this.port_, '127.0.0.1'); enableServerDestroy(this.server_); diff --git a/packages/lib/onedrive-api-node-utils.ts b/packages/lib/onedrive-api-node-utils.ts index ce43a0276d..219a58f1fe 100644 --- a/packages/lib/onedrive-api-node-utils.ts +++ b/packages/lib/onedrive-api-node-utils.ts @@ -5,6 +5,7 @@ import OneDriveApi from './onedrive-api'; import * as http from 'http'; import * as urlParser from 'url'; +import { randomBytes } from 'crypto'; const enableServerDestroy = require('server-destroy'); interface DestroyableServer extends http.Server { @@ -57,7 +58,8 @@ export class OneDriveApiNodeUtils { const port = await findAvailablePort(require('tcp-port-used'), this.possibleOAuthDancePorts(), 0); if (!port) throw new Error(_('All potential ports are in use - please report the issue at %s', 'https://github.com/laurent22/joplin')); - const authCodeUrl = this.api().authCodeUrl(`http://localhost:${port}`); + const state = randomBytes(32).toString('base64url'); + const authCodeUrl = this.api().authCodeUrl(`http://localhost:${port}`, state); return new Promise((resolve, reject) => { this.oauthServer_ = http.createServer() as DestroyableServer; @@ -91,6 +93,7 @@ export class OneDriveApiNodeUtils { }; if (!query.code) return writeResponse(400, '"code" query parameter is missing'); + if (query.state !== state) return writeResponse(400, '"state" query parameter is invalid'); this.api() .execTokenRequest(query.code as string, `http://localhost:${port.toString()}`) @@ -118,7 +121,7 @@ export class OneDriveApiNodeUtils { } }); - this.oauthServer_.listen(port); + this.oauthServer_.listen(port, '127.0.0.1'); enableServerDestroy(this.oauthServer_); diff --git a/packages/lib/onedrive-api.ts b/packages/lib/onedrive-api.ts index 386aa28a84..7fa827917d 100644 --- a/packages/lib/onedrive-api.ts +++ b/packages/lib/onedrive-api.ts @@ -92,14 +92,15 @@ export default class OneDriveApi { return `${r.parentReference.path}/${r.name}`; } - public authCodeUrl(redirectUri: string) { - const query = { + public authCodeUrl(redirectUri: string, state: string | null = null) { + const query: Record = { client_id: this.clientId_, scope: 'files.readwrite offline_access sites.readwrite.all', response_type: 'code', redirect_uri: redirectUri, prompt: 'login', }; + if (state) query.state = state; return `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?${stringify(query)}`; }