From d61a846db111f203963efe7cda8ac7af96da1880 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Mon, 5 Oct 2026 01:48:16 -0700 Subject: [PATCH] Accept matching duplicate client credentials from Shale Assisted-by: gpt-6 --- dashboard/src/oidc.rs | 13 +++++++++---- dashboard/web/components/Explorer.tsx | 11 +++-------- tools/dashboard-oidc-test.py | 7 ++++++- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/dashboard/src/oidc.rs b/dashboard/src/oidc.rs index b0efeba4717e5a82db699875d2934f927479a131..3458f8397d674de3b8c30b654d8020ecd041db03 100644 --- a/dashboard/src/oidc.rs +++ b/dashboard/src/oidc.rs @@ -313,9 +313,6 @@ fn authenticated_client( form: &HashMap, ) -> Result { let (id, secret) = if let Some(header) = headers.get("authorization") { - if form.contains_key("client_secret") { - return Err(invalid("invalid_request")); - } let header = header .to_str() .ok() @@ -337,7 +334,15 @@ fn authenticated_client( .1 .into_owned() }; - (decode(id), decode(secret)) + let (id, secret) = (decode(id), decode(secret)); + // Shale repeats Basic credentials in its form; require the same secret. + if form.get("client_secret").is_some_and(|supplied| { + !(id == "shale" || id.starts_with("shale-preview-")) + || mcp::hash(supplied) != mcp::hash(&secret) + }) { + return Err(invalid("invalid_request")); + } + (id, secret) } else { ( form.get("client_id").cloned().unwrap_or_default(), diff --git a/dashboard/web/components/Explorer.tsx b/dashboard/web/components/Explorer.tsx index bde2fb70480ce14f5667d0b3978cd6b0037b40cf..88c7aa793c543d9af22e75436cfb87fa882a3f77 100644 --- a/dashboard/web/components/Explorer.tsx +++ b/dashboard/web/components/Explorer.tsx @@ -235,7 +235,6 @@ export function Explorer(props: { id: string; client: Client; root: string }) { let anchor: string | undefined; let table: HTMLTableElement | undefined; let filesScroll!: HTMLDivElement; - let lastCursor: string | undefined; let revealCursor: string | undefined; let patternInput!: HTMLInputElement; @@ -342,13 +341,9 @@ export function Explorer(props: { id: string; client: Client; root: string }) { refreshTree(); })); - // A listing update must not scroll back to an old selection after the user scrolled elsewhere. - // Keep a new cursor pending until its row arrives, then reveal it once in this scroller only. - createEffect(on([cursor, rows], ([path, all]) => { - if (path !== lastCursor) { - lastCursor = path; - revealCursor = path; - } + // Folder updates must not scroll back to a selection the user has scrolled away from. + createEffect(on([cursor, rows], ([path, all], before) => { + if (path !== before?.[0]) revealCursor = path; if (!path || revealCursor !== path) return; const index = all.findIndex((row) => row.path === path); if (index < 0) return; diff --git a/tools/dashboard-oidc-test.py b/tools/dashboard-oidc-test.py index a7e58ff10a301f3ec7d07426a7fca42479badf2f..20b58107477409d38f99bb4d3b3cae6705f50cd2 100644 --- a/tools/dashboard-oidc-test.py +++ b/tools/dashboard-oidc-test.py @@ -59,6 +59,7 @@ def main(): provision('other', ['https://jelly.paperclover.net/callback']) provision('bad', ['https://evil.example/callback'], status=1) provision('bad', ['https://shale.paperclover.net/*'], status=1) + provision('shale-preview-bad', ['https://jelly.paperclover.net/-/callback'], guests=True, status=1) with socket.socket() as available: available.bind(('127.0.0.1', 0)); port = available.getsockname()[1] environment['PORT'] = str(port) @@ -121,7 +122,11 @@ def main(): 'redirect_uri': callback, 'code': code(), 'code_verifier': verifier} for change in [{'client_secret': 'wrong'}, {'client_id': 'other'}, {'code_verifier': 'wrong'}, {'redirect_uri': 'https://evil.example/'}]: request('/auth/oidc/token', 'POST', {**exchange, **change}, status=400, form=True) - tokens = request('/auth/oidc/token', 'POST', exchange, form=True) + other_basic = {'Authorization': 'Basic ' + base64.b64encode(('other:' + secret).encode()).decode()} + assert request('/auth/oidc/token', 'POST', {'client_id':'other','client_secret':secret,'grant_type':'authorization_code'}, extra=other_basic, status=400, form=True)['error'] == 'invalid_request' + basic_header = {'Authorization': 'Basic ' + base64.b64encode(('shale:' + secret).encode()).decode()} + request('/auth/oidc/token', 'POST', {**exchange, 'client_secret': 'different'}, extra=basic_header, status=400, form=True) + tokens = request('/auth/oidc/token', 'POST', exchange, extra=basic_header, form=True) request('/auth/oidc/token', 'POST', exchange, status=400, form=True) parts = tokens['id_token'].split('.') key.verify(decode(parts[2]), (parts[0] + '.' + parts[1]).encode(), padding.PKCS1v15(), hashes.SHA256()) -- 2.54.0