@@ -13,6 +13,7 @@ use axum_extra::extract::Form;
1313use  axum_messages:: Messages ; 
1414use  openidconnect as  oidc; 
1515use  password_auth:: verify_password; 
16+ use  percent_encoding:: { NON_ALPHANUMERIC ,  utf8_percent_encode} ; 
1617use  serde:: Deserialize ; 
1718use  tower_sessions:: Session ; 
1819use  tracing:: instrument; 
@@ -78,6 +79,10 @@ pub(crate) async fn log_in_page(
7879    // Get community information 
7980    let  community = db. get_community ( community_id) . await ?; 
8081
82+     // Sanitize and encode the next url (if any) 
83+     let  next_url =
84+         sanitize_next_url ( query. get ( "next_url" ) . map ( String :: as_str) ) . map ( |value| encode_next_url ( & value) ) ; 
85+ 
8186    // Prepare template 
8287    let  template = templates:: auth:: LogInPage  { 
8388        community, 
@@ -87,7 +92,7 @@ pub(crate) async fn log_in_page(
8792        path :  LOG_IN_URL . to_string ( ) , 
8893        user :  User :: default ( ) , 
8994
90-         next_url :  query . get ( "next_url" ) . cloned ( ) , 
95+         next_url, 
9196    } ; 
9297
9398    Ok ( Html ( template. render ( ) ?) . into_response ( ) ) 
@@ -111,6 +116,10 @@ pub(crate) async fn sign_up_page(
111116    // Get community information 
112117    let  community = db. get_community ( community_id) . await ?; 
113118
119+     // Sanitize and encode the next url (if any) 
120+     let  next_url =
121+         sanitize_next_url ( query. get ( "next_url" ) . map ( String :: as_str) ) . map ( |value| encode_next_url ( & value) ) ; 
122+ 
114123    // Prepare template 
115124    let  template = templates:: auth:: SignUpPage  { 
116125        community, 
@@ -120,7 +129,7 @@ pub(crate) async fn sign_up_page(
120129        path :  SIGN_UP_URL . to_string ( ) , 
121130        user :  User :: default ( ) , 
122131
123-         next_url :  query . get ( "next_url" ) . cloned ( ) , 
132+         next_url, 
124133    } ; 
125134
126135    Ok ( Html ( template. render ( ) ?) . into_response ( ) ) 
@@ -150,6 +159,9 @@ pub(crate) async fn log_in(
150159    Query ( query) :  Query < HashMap < String ,  String > > , 
151160    Form ( login_form) :  Form < LoginForm > , 
152161)  -> Result < impl  IntoResponse ,  HandlerError >  { 
162+     // Sanitize next url 
163+     let  next_url = sanitize_next_url ( query. get ( "next_url" ) . map ( String :: as_str) ) ; 
164+ 
153165    // Authenticate user 
154166    let  creds = PasswordCredentials  { 
155167        community_id, 
@@ -162,7 +174,7 @@ pub(crate) async fn log_in(
162174        . map_err ( |e| HandlerError :: Auth ( e. to_string ( ) ) ) ?
163175    else  { 
164176        messages. error ( "Invalid credentials. Please make sure you have verified your email address." ) ; 
165-         let  log_in_url = get_log_in_url ( query . get ( "next_url" ) ) ; 
177+         let  log_in_url = get_log_in_url ( next_url . as_deref ( ) ) ; 
166178        return  Ok ( Redirect :: to ( & log_in_url) ) ; 
167179    } ; 
168180
@@ -178,13 +190,7 @@ pub(crate) async fn log_in(
178190        session. insert ( SELECTED_GROUP_ID_KEY ,  groups[ 0 ] . group_id ) . await ?; 
179191    } 
180192
181-     // Prepare next url 
182-     let  next_url = if  let  Some ( next_url)  = query. get ( "next_url" )  { 
183-         next_url
184-     }  else  { 
185-         "/" 
186-     } ; 
187- 
193+     let  next_url = next_url. as_deref ( ) . unwrap_or ( "/" ) ; 
188194    Ok ( Redirect :: to ( next_url) ) 
189195} 
190196
@@ -223,8 +229,12 @@ pub(crate) async fn oauth2_callback(
223229    } 
224230
225231    // Get next url from session (if any) 
226-     let  next_url = session. remove :: < Option < String > > ( NEXT_URL_KEY ) . await ?. flatten ( ) ; 
227-     let  log_in_url = get_log_in_url ( next_url. as_ref ( ) ) ; 
232+     let  next_url = session
233+         . remove :: < Option < String > > ( NEXT_URL_KEY ) 
234+         . await ?
235+         . flatten ( ) 
236+         . and_then ( |value| sanitize_next_url ( Some ( value. as_str ( ) ) ) ) ; 
237+     let  log_in_url = get_log_in_url ( next_url. as_deref ( ) ) ; 
228238
229239    // Authenticate user 
230240    let  creds = OAuth2Credentials  { 
@@ -256,10 +266,8 @@ pub(crate) async fn oauth2_callback(
256266        session. insert ( SELECTED_GROUP_ID_KEY ,  groups[ 0 ] . group_id ) . await ?; 
257267    } 
258268
259-     // Prepare next url 
260-     let  next_url = next_url. unwrap_or ( "/" . to_string ( ) ) ; 
261- 
262-     Ok ( Redirect :: to ( & next_url) ) 
269+     let  next_url = next_url. as_deref ( ) . unwrap_or ( "/" ) ; 
270+     Ok ( Redirect :: to ( next_url) ) 
263271} 
264272
265273/// Handler that redirects the user to the oauth2 provider. 
@@ -276,6 +284,9 @@ pub(crate) async fn oauth2_redirect(
276284    } 
277285    let  ( authorize_url,  csrf_state)  = builder. url ( ) ; 
278286
287+     // Sanitize the next url (if provided) 
288+     let  next_url = sanitize_next_url ( next_url. as_deref ( ) ) ; 
289+ 
279290    // Save the csrf state and next url in the session 
280291    session. insert ( OAUTH2_CSRF_STATE_KEY ,  csrf_state. secret ( ) ) . await ?; 
281292    session. insert ( NEXT_URL_KEY ,  next_url) . await ?; 
@@ -314,8 +325,12 @@ pub(crate) async fn oidc_callback(
314325    } ; 
315326
316327    // Get next url from session (if any) 
317-     let  next_url = session. remove :: < Option < String > > ( NEXT_URL_KEY ) . await ?. flatten ( ) ; 
318-     let  log_in_url = get_log_in_url ( next_url. as_ref ( ) ) ; 
328+     let  next_url = session
329+         . remove :: < Option < String > > ( NEXT_URL_KEY ) 
330+         . await ?
331+         . flatten ( ) 
332+         . and_then ( |value| sanitize_next_url ( Some ( value. as_str ( ) ) ) ) ; 
333+     let  log_in_url = get_log_in_url ( next_url. as_deref ( ) ) ; 
319334
320335    // Authenticate user 
321336    let  creds = OidcCredentials  { 
@@ -351,10 +366,8 @@ pub(crate) async fn oidc_callback(
351366    // Track auth provider in the session 
352367    session. insert ( AUTH_PROVIDER_KEY ,  provider) . await ?; 
353368
354-     // Prepare next url 
355-     let  next_url = next_url. unwrap_or ( "/" . to_string ( ) ) ; 
356- 
357-     Ok ( Redirect :: to ( & next_url) ) 
369+     let  next_url = next_url. as_deref ( ) . unwrap_or ( "/" ) ; 
370+     Ok ( Redirect :: to ( next_url) ) 
358371} 
359372
360373/// Handler that redirects the user to the oidc provider. 
@@ -375,6 +388,9 @@ pub(crate) async fn oidc_redirect(
375388    } 
376389    let  ( authorize_url,  csrf_state,  nonce)  = builder. url ( ) ; 
377390
391+     // Sanitize the next url (if provided) 
392+     let  next_url = sanitize_next_url ( next_url. as_deref ( ) ) ; 
393+ 
378394    // Save the csrf state, nonce and next url in the session 
379395    session. insert ( OAUTH2_CSRF_STATE_KEY ,  csrf_state. secret ( ) ) . await ?; 
380396    session. insert ( OIDC_NONCE_KEY ,  nonce. secret ( ) ) . await ?; 
@@ -429,7 +445,8 @@ pub(crate) async fn sign_up(
429445    } 
430446
431447    // Redirect to the log in page on success 
432-     let  log_in_url = get_log_in_url ( query. get ( "next_url" ) ) ; 
448+     let  next_url = sanitize_next_url ( query. get ( "next_url" ) . map ( String :: as_str) ) ; 
449+     let  log_in_url = get_log_in_url ( next_url. as_deref ( ) ) ; 
433450    Ok ( Redirect :: to ( & log_in_url) . into_response ( ) ) 
434451} 
435452
@@ -504,15 +521,34 @@ pub(crate) async fn verify_email(
504521    Ok ( Redirect :: to ( LOG_IN_URL ) ) 
505522} 
506523
524+ // Helpers. 
525+ 
526+ /// Percent-encode a `next_url` so it can be safely embedded in a query string. 
527+ fn  encode_next_url ( next_url :  & str )  -> String  { 
528+     utf8_percent_encode ( next_url,  NON_ALPHANUMERIC ) . to_string ( ) 
529+ } 
530+ 
507531/// Get the log in url including the next url if provided. 
508- fn  get_log_in_url ( next_url :  Option < & String > )  -> String  { 
532+ fn  get_log_in_url ( next_url :  Option < & str > )  -> String  { 
509533    let  mut  log_in_url = LOG_IN_URL . to_string ( ) ; 
510-     if  let  Some ( next_url)  = next_url { 
511-         log_in_url = format ! ( "{log_in_url}?next_url={next_url}"  ) ; 
534+     if  let  Some ( next_url)  = sanitize_next_url ( next_url)  { 
535+         log_in_url = format ! ( "{log_in_url}?next_url={}"  ,  encode_next_url ( & next_url ) ) ; 
512536    } 
513537    log_in_url
514538} 
515539
540+ /// Sanitize a `next_url` value ensuring it points to an in-site path. 
541+ fn  sanitize_next_url ( next_url :  Option < & str > )  -> Option < String >  { 
542+     let  value = next_url?. trim ( ) ; 
543+     if  value. is_empty ( )  { 
544+         return  None ; 
545+     } 
546+     if  !value. starts_with ( '/' )  || value. starts_with ( "//" )  { 
547+         return  None ; 
548+     } 
549+     Some ( value. to_string ( ) ) 
550+ } 
551+ 
516552// Types. 
517553
518554/// Login form data from the user. 
@@ -1606,7 +1642,7 @@ mod tests {
16061642        assert_eq ! ( parts. status,  StatusCode :: SEE_OTHER ) ; 
16071643        assert_eq ! ( 
16081644            parts. headers. get( LOCATION ) . unwrap( ) , 
1609-             & HeaderValue :: from_static( "/log-in?next_url=/welcome " ) , 
1645+             & HeaderValue :: from_static( "/log-in?next_url=%2Fwelcome " ) , 
16101646        ) ; 
16111647        assert ! ( bytes. is_empty( ) ) ; 
16121648    } 
@@ -2033,8 +2069,33 @@ mod tests {
20332069
20342070    #[ test]  
20352071    fn  test_get_log_in_url_with_next ( )  { 
2036-         let  url = get_log_in_url ( Some ( & "/dashboard" . to_string ( ) ) ) ; 
2037-         assert_eq ! ( url,  "/log-in?next_url=/dashboard" ) ; 
2072+         let  url = get_log_in_url ( Some ( "/dashboard" ) ) ; 
2073+         assert_eq ! ( url,  "/log-in?next_url=%2Fdashboard" ) ; 
2074+     } 
2075+ 
2076+     #[ test]  
2077+     fn  test_sanitize_next_url_accepts_internal_paths ( )  { 
2078+         assert_eq ! ( 
2079+             sanitize_next_url( Some ( "/dashboard" ) ) , 
2080+             Some ( "/dashboard" . to_string( ) ) 
2081+         ) ; 
2082+         assert_eq ! ( 
2083+             sanitize_next_url( Some ( "/groups?page=2#section" ) ) , 
2084+             Some ( "/groups?page=2#section" . to_string( ) ) 
2085+         ) ; 
2086+         assert_eq ! ( 
2087+             sanitize_next_url( Some ( "   /profile  " ) ) , 
2088+             Some ( "/profile" . to_string( ) ) 
2089+         ) ; 
2090+     } 
2091+ 
2092+     #[ test]  
2093+     fn  test_sanitize_next_url_rejects_external_paths ( )  { 
2094+         assert_eq ! ( sanitize_next_url( Some ( "" ) ) ,  None ) ; 
2095+         assert_eq ! ( sanitize_next_url( Some ( "https://evil.example" ) ) ,  None ) ; 
2096+         assert_eq ! ( sanitize_next_url( Some ( "//evil.example" ) ) ,  None ) ; 
2097+         assert_eq ! ( sanitize_next_url( Some ( "javascript:alert(1)" ) ) ,  None ) ; 
2098+         assert_eq ! ( sanitize_next_url( Some ( "relative/path" ) ) ,  None ) ; 
20382099    } 
20392100
20402101    #[ tokio:: test]  
0 commit comments