| author | |
| committer | |
| log | a321e9b853d7d9eb4d05c508ee3c308042855c6e |
| tree | f8507bfc05adad57d7cdad0f17a018c6f67e7d9d |
| parent | c3d894fed29df8d1a864de6a5bd1d0fda57882f5 |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
Keep Shale session revocation, then clear the current Snowglobe session and return to Shale. Require the Shale referrer and verify session-cookie replay is refused.
Assisted-by: gpt-63 files changed, 51 insertions(+), 9 deletions(-)
dashboard/src/auth.rs+34-8| ... | @@ -705,21 +705,40 @@ pub async fn route(State(app): State<Arc<App>>, request: Request) -> Result<Resp | ... | @@ -705,21 +705,40 @@ pub async fn route(State(app): State<Arc<App>>, request: Request) -> Result<Resp |
| 705 | ) | 705 | ) |
| 706 | .into_response()); | 706 | .into_response()); |
| 707 | } | 707 | } |
| 708 | if path == "/auth/sign-out" || path == "/auth/file/sign-out" { | 708 | let shale_logout = path == "/auth/shale/sign-out"; |
| 709 | if path == "/auth/sign-out" || path == "/auth/file/sign-out" || shale_logout { | ||
| 709 | if method == Method::GET && path == "/auth/file/sign-out" { | 710 | if method == Method::GET && path == "/auth/file/sign-out" { |
| 710 | return Ok(axum::response::Html("<!doctype html><html><meta name=viewport content='width=device-width'><title>Sign out of Files</title><body><form method=post action='/auth/file/sign-out'><button>sign out of Files</button></form></body></html>").into_response()); | 711 | return Ok(axum::response::Html("<!doctype html><html><meta name=viewport content='width=device-width'><title>Sign out of Files</title><body><form method=post action='/auth/file/sign-out'><button>sign out of Files</button></form></body></html>").into_response()); |
| 711 | } | 712 | } |
| 712 | if method != Method::POST { | 713 | if method |
| 714 | != (if shale_logout { | ||
| 715 | Method::GET | ||
| 716 | } else { | ||
| 717 | Method::POST | ||
| 718 | }) | ||
| 719 | { | ||
| 713 | return Err(Error::new(405, "Use the sign-out button.")); | 720 | return Err(Error::new(405, "Use the sign-out button.")); |
| 714 | } | 721 | } |
| 715 | let expected = if path.contains("/file/") { | 722 | let expected = if shale_logout { |
| 723 | &app.shale.origin | ||
| 724 | } else if path.contains("/file/") { | ||
| 716 | &auth.file | 725 | &auth.file |
| 717 | } else { | 726 | } else { |
| 718 | &auth.origin | 727 | &auth.origin |
| 719 | }; | 728 | }; |
| 720 | if headers.get("origin").and_then(|v| v.to_str().ok()) | 729 | let source = if shale_logout { |
| 721 | != Some(expected.origin().ascii_serialization().as_str()) | 730 | headers |
| 722 | { | 731 | .get("referer") |
| 732 | .and_then(|v| v.to_str().ok()) | ||
| 733 | .and_then(|v| url::Url::parse(v).ok()) | ||
| 734 | .map(|v| v.origin().ascii_serialization()) | ||
| 735 | } else { | ||
| 736 | headers | ||
| 737 | .get("origin") | ||
| 738 | .and_then(|v| v.to_str().ok()) | ||
| 739 | .map(str::to_owned) | ||
| 740 | }; | ||
| 741 | if source.as_deref() != Some(expected.origin().ascii_serialization().as_str()) { | ||
| 723 | return Err(Error::new(403, "Open your account to sign out.")); | 742 | return Err(Error::new(403, "Open your account to sign out.")); |
| 724 | } | 743 | } |
| 725 | if let Some(token) = cookie(&headers, COOKIE) { | 744 | if let Some(token) = cookie(&headers, COOKIE) { |
| ... | @@ -728,12 +747,19 @@ pub async fn route(State(app): State<Arc<App>>, request: Request) -> Result<Resp | ... | @@ -728,12 +747,19 @@ pub async fn route(State(app): State<Arc<App>>, request: Request) -> Result<Resp |
| 728 | .unwrap() | 747 | .unwrap() |
| 729 | .execute("DELETE FROM sessions WHERE hash=?", [mcp::hash(&token)])?; | 748 | .execute("DELETE FROM sessions WHERE hash=?", [mcp::hash(&token)])?; |
| 730 | } | 749 | } |
| 731 | if path.contains("/file/") { | 750 | if path.contains("/file/") || shale_logout { |
| 732 | return Ok(( | 751 | return Ok(( |
| 733 | StatusCode::SEE_OTHER, | 752 | StatusCode::SEE_OTHER, |
| 734 | [ | 753 | [ |
| 735 | ("set-cookie", set_cookie(COOKIE, "", 0)), | 754 | ("set-cookie", set_cookie(COOKIE, "", 0)), |
| 736 | ("location", "/".into()), | 755 | ( |
| 756 | "location", | ||
| 757 | if shale_logout { | ||
| 758 | app.shale.origin.to_string() | ||
| 759 | } else { | ||
| 760 | "/".into() | ||
| 761 | }, | ||
| 762 | ), | ||
| 737 | ], | 763 | ], |
| 738 | ) | 764 | ) |
| 739 | .into_response()); | 765 | .into_response()); |
tools/dashboard-auth-test.py+13-1| ... | @@ -197,10 +197,22 @@ def main(): | ... | @@ -197,10 +197,22 @@ def main(): |
| 197 | stop(); start() | 197 | stop(); start() |
| 198 | request('/api/me', cookies=cookies) | 198 | request('/api/me', cookies=cookies) |
| 199 | request('/auth/file/check', cookies=file_cookies, status=204, host=file) | 199 | request('/auth/file/check', cookies=file_cookies, status=204, host=file) |
| 200 | shale_session = {}; shale_csrf = request('/auth/status', cookies=shale_session)['csrf'] | ||
| 201 | request('/auth/password', 'POST', {**login, 'csrf': shale_csrf}, shale_session) | ||
| 202 | captured = shale_session.copy() | ||
| 203 | request('/auth/shale/sign-out', cookies=shale_session, status=403) | ||
| 204 | request('/auth/shale/sign-out', cookies=shale_session, status=403, extra={'Referer': 'https://evil.example/'}) | ||
| 205 | request('/auth/shale/sign-out', cookies=shale_session, status=403, extra={'Referer': 'https://shale.paperclover.net.evil.example/'}) | ||
| 206 | request('/auth/shale/sign-out', 'POST', {}, shale_session, 405, {'Referer': 'https://shale.paperclover.net/'}) | ||
| 207 | request('/api/me', cookies=shale_session) | ||
| 208 | ended = request('/auth/shale/sign-out', cookies=shale_session, status=303, extra={'Referer': 'https://shale.paperclover.net/'}) | ||
| 209 | assert ended['location'] == 'https://shale.paperclover.net/' and shale_session['__Host-snow-session'] == '' | ||
| 210 | request('/api/me', cookies=captured, status=401) | ||
| 211 | request('/api/me', cookies=cookies) | ||
| 200 | request('/api/users/' + actor + '/logout', 'POST', {}, cookies, 204) | 212 | request('/api/users/' + actor + '/logout', 'POST', {}, cookies, 204) |
| 201 | request('/api/me', cookies=cookies, status=401) | 213 | request('/api/me', cookies=cookies, status=401) |
| 202 | request('/auth/file/check', cookies=file_cookies, status=401, host=file) | 214 | request('/auth/file/check', cookies=file_cookies, status=401, host=file) |
| 203 | print(json.dumps({'import': 'passed', 'password': 'passed', 'signed_legacy_passkey': 'passed', 'username_free_passkey': 'passed', 'remember_me': 'passed', 'registration': 'passed', 'csrf_and_header_forgery': 'passed', 'file_handoff_replay_and_binding': 'passed', 'invitation_one_use_and_revocation': 'passed', 'restart_and_logout': 'passed'})) | 215 | print(json.dumps({'import': 'passed', 'password': 'passed', 'signed_legacy_passkey': 'passed', 'username_free_passkey': 'passed', 'remember_me': 'passed', 'registration': 'passed', 'csrf_and_header_forgery': 'passed', 'file_handoff_replay_and_binding': 'passed', 'invitation_one_use_and_revocation': 'passed', 'restart_and_logout': 'passed', 'shale_logout_and_cookie_replay': 'passed'})) |
| 204 | finally: | 216 | finally: |
| 205 | if server and server.poll() is None: stop() | 217 | if server and server.poll() is None: stop() |
| 206 | log.close() | 218 | log.close() |
tools/router.py+4| ... | @@ -196,6 +196,10 @@ def render(token): | ... | @@ -196,6 +196,10 @@ def render(token): |
| 196 | lines += shale_attachment_routes(" ".join(sorted(route["upstreams"])), | 196 | lines += shale_attachment_routes(" ".join(sorted(route["upstreams"])), |
| 197 | "/srv/staging/" + service) | 197 | "/srv/staging/" + service) |
| 198 | if service == "shale": | 198 | if service == "shale": |
| 199 | lines += [" handle /-/logout {", f" reverse_proxy {' '.join(sorted(route['upstreams']))} {{", | ||
| 200 | f" header_down Location https://snowglobe.{os.environ['STUDIO_DOMAIN']}/auth/shale/sign-out", | ||
| 201 | " @logged_out status 200 302 303 404", " handle_response @logged_out {", | ||
| 202 | " copy_response 303", " }", " }", " }"] | ||
| 199 | lines += shale_attachment_routes(" ".join(sorted(route["upstreams"]))) | 203 | lines += shale_attachment_routes(" ".join(sorted(route["upstreams"]))) |
| 200 | lines += [" @userscript path_regexp userscript ^/userscripts/discord-pluralkit-predict(/.*)?$", | 204 | lines += [" @userscript path_regexp userscript ^/userscripts/discord-pluralkit-predict(/.*)?$", |
| 201 | " redir @userscript /discord-pluralkit-predict{re.userscript.1}?{query} 308"] | 205 | " redir @userscript /discord-pluralkit-predict{re.userscript.1}?{query} 308"] |