@@ -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;
@@ -69,13 +70,17 @@ pub(crate) async fn log_in_page(
6970 return Ok ( Redirect :: to ( "/" ) . into_response ( ) ) ;
7071 }
7172
73+ // Sanitize and encode the next url (if provided)
74+ let next_url =
75+ sanitize_next_url ( query. get ( "next_url" ) . map ( String :: as_str) ) . map ( |value| encode_next_url ( & value) ) ;
76+
7277 // Prepare template
7378 let template = templates:: auth:: LogInPage {
7479 auth_provider : None ,
7580 login : cfg. login . clone ( ) ,
7681 cfg : cfg. into ( ) ,
7782 messages : messages. into_iter ( ) . collect ( ) ,
78- next_url : query . get ( "next_url" ) . cloned ( ) ,
83+ next_url,
7984 page_id : PageId :: LogIn ,
8085 user : User :: default ( ) ,
8186 } ;
@@ -96,13 +101,17 @@ pub(crate) async fn sign_up_page(
96101 return Ok ( Redirect :: to ( "/" ) . into_response ( ) ) ;
97102 }
98103
104+ // Sanitize and encode the next url (if provided)
105+ let next_url =
106+ sanitize_next_url ( query. get ( "next_url" ) . map ( String :: as_str) ) . map ( |value| encode_next_url ( & value) ) ;
107+
99108 // Prepare template
100109 let template = templates:: auth:: SignUpPage {
101110 auth_provider : None ,
102111 login : cfg. login . clone ( ) ,
103112 cfg : cfg. into ( ) ,
104113 messages : messages. into_iter ( ) . collect ( ) ,
105- next_url : query . get ( "next_url" ) . cloned ( ) ,
114+ next_url,
106115 page_id : PageId :: SignUp ,
107116 user : User :: default ( ) ,
108117 } ;
@@ -122,14 +131,17 @@ pub(crate) async fn log_in(
122131 State ( db) : State < DynDB > ,
123132 Form ( creds) : Form < PasswordCredentials > ,
124133) -> Result < impl IntoResponse , HandlerError > {
134+ // Sanitize next url
135+ let next_url = sanitize_next_url ( query. get ( "next_url" ) . map ( String :: as_str) ) ;
136+
125137 // Authenticate user
126138 let Some ( user) = auth_session
127139 . authenticate ( Credentials :: Password ( creds. clone ( ) ) )
128140 . await
129141 . map_err ( |e| HandlerError :: Auth ( e. to_string ( ) ) ) ?
130142 else {
131143 messages. error ( "Invalid credentials. Please make sure you have verified your email address." ) ;
132- let log_in_url = get_log_in_url ( query . get ( "next_url" ) ) ;
144+ let log_in_url = get_log_in_url ( next_url . as_deref ( ) ) ;
133145 return Ok ( Redirect :: to ( & log_in_url) ) ;
134146 } ;
135147
@@ -147,13 +159,7 @@ pub(crate) async fn log_in(
147159 . await ?;
148160 }
149161
150- // Prepare next url
151- let next_url = if let Some ( next_url) = query. get ( "next_url" ) {
152- next_url
153- } else {
154- "/"
155- } ;
156-
162+ let next_url = next_url. as_deref ( ) . unwrap_or ( "/" ) ;
157163 Ok ( Redirect :: to ( next_url) )
158164}
159165
@@ -191,8 +197,12 @@ pub(crate) async fn oauth2_callback(
191197 }
192198
193199 // Get next url from session (if any)
194- let next_url = session. remove :: < Option < String > > ( NEXT_URL_KEY ) . await ?. flatten ( ) ;
195- let log_in_url = get_log_in_url ( next_url. as_ref ( ) ) ;
200+ let next_url = session
201+ . remove :: < Option < String > > ( NEXT_URL_KEY )
202+ . await ?
203+ . flatten ( )
204+ . and_then ( |value| sanitize_next_url ( Some ( value. as_str ( ) ) ) ) ;
205+ let log_in_url = get_log_in_url ( next_url. as_deref ( ) ) ;
196206
197207 // Authenticate user
198208 let creds = OAuth2Credentials { code, provider } ;
@@ -222,10 +232,8 @@ pub(crate) async fn oauth2_callback(
222232 . await ?;
223233 }
224234
225- // Prepare next url
226- let next_url = next_url. unwrap_or ( "/" . to_string ( ) ) ;
227-
228- Ok ( Redirect :: to ( & next_url) )
235+ let next_url = next_url. as_deref ( ) . unwrap_or ( "/" ) ;
236+ Ok ( Redirect :: to ( next_url) )
229237}
230238
231239/// Handler that redirects the user to the oauth2 provider.
@@ -242,6 +250,9 @@ pub(crate) async fn oauth2_redirect(
242250 }
243251 let ( authorize_url, csrf_state) = builder. url ( ) ;
244252
253+ // Sanitize the next url (if any)
254+ let next_url = sanitize_next_url ( next_url. as_deref ( ) ) ;
255+
245256 // Save the csrf state and next url in the session
246257 session. insert ( OAUTH2_CSRF_STATE_KEY , csrf_state. secret ( ) ) . await ?;
247258 session. insert ( NEXT_URL_KEY , next_url) . await ?;
@@ -279,8 +290,12 @@ pub(crate) async fn oidc_callback(
279290 } ;
280291
281292 // Get next url from session (if any)
282- let next_url = session. remove :: < Option < String > > ( NEXT_URL_KEY ) . await ?. flatten ( ) ;
283- let log_in_url = get_log_in_url ( next_url. as_ref ( ) ) ;
293+ let next_url = session
294+ . remove :: < Option < String > > ( NEXT_URL_KEY )
295+ . await ?
296+ . flatten ( )
297+ . and_then ( |value| sanitize_next_url ( Some ( value. as_str ( ) ) ) ) ;
298+ let log_in_url = get_log_in_url ( next_url. as_deref ( ) ) ;
284299
285300 // Authenticate user
286301 let creds = OidcCredentials {
@@ -317,10 +332,8 @@ pub(crate) async fn oidc_callback(
317332 . await ?;
318333 }
319334
320- // Prepare next url
321- let next_url = next_url. unwrap_or ( "/" . to_string ( ) ) ;
322-
323- Ok ( Redirect :: to ( & next_url) )
335+ let next_url = next_url. as_deref ( ) . unwrap_or ( "/" ) ;
336+ Ok ( Redirect :: to ( next_url) )
324337}
325338
326339/// Handler that redirects the user to the oidc provider.
@@ -341,6 +354,9 @@ pub(crate) async fn oidc_redirect(
341354 }
342355 let ( authorize_url, csrf_state, nonce) = builder. url ( ) ;
343356
357+ // Sanitize the next url (if any)
358+ let next_url = sanitize_next_url ( next_url. as_deref ( ) ) ;
359+
344360 // Save the csrf state, nonce and next url in the session
345361 session. insert ( OAUTH2_CSRF_STATE_KEY , csrf_state. secret ( ) ) . await ?;
346362 session. insert ( OIDC_NONCE_KEY , nonce. secret ( ) ) . await ?;
@@ -393,7 +409,8 @@ pub(crate) async fn sign_up(
393409 }
394410
395411 // Redirect to the log in page on success
396- let log_in_url = get_log_in_url ( query. get ( "next_url" ) ) ;
412+ let next_url = sanitize_next_url ( query. get ( "next_url" ) . map ( String :: as_str) ) ;
413+ let log_in_url = get_log_in_url ( next_url. as_deref ( ) ) ;
397414 Ok ( Redirect :: to ( & log_in_url) . into_response ( ) )
398415}
399416
@@ -466,15 +483,6 @@ pub(crate) async fn verify_email(
466483 Ok ( Redirect :: to ( LOG_IN_URL ) . into_response ( ) )
467484}
468485
469- /// Get the log in url including the next url if provided.
470- fn get_log_in_url ( next_url : Option < & String > ) -> String {
471- let mut log_in_url = LOG_IN_URL . to_string ( ) ;
472- if let Some ( next_url) = next_url {
473- log_in_url = format ! ( "{log_in_url}?next_url={next_url}" ) ;
474- }
475- log_in_url
476- }
477-
478486// Deserialization helpers.
479487
480488/// `OAuth2` authorization response containing code and CSRF state.
@@ -632,3 +640,31 @@ pub(crate) async fn user_owns_job(
632640
633641 next. run ( request) . await . into_response ( )
634642}
643+
644+ // Helpers.
645+
646+ /// Percent-encode a `next_url` so it can be safely embedded in a query string.
647+ fn encode_next_url ( next_url : & str ) -> String {
648+ utf8_percent_encode ( next_url, NON_ALPHANUMERIC ) . to_string ( )
649+ }
650+
651+ /// Get the log in url including the next url if provided.
652+ fn get_log_in_url ( next_url : Option < & str > ) -> String {
653+ let mut log_in_url = LOG_IN_URL . to_string ( ) ;
654+ if let Some ( next_url) = sanitize_next_url ( next_url) {
655+ log_in_url = format ! ( "{log_in_url}?next_url={}" , encode_next_url( & next_url) ) ;
656+ }
657+ log_in_url
658+ }
659+
660+ /// Sanitize a `next_url` value ensuring it points to an in-site path.
661+ fn sanitize_next_url ( next_url : Option < & str > ) -> Option < String > {
662+ let value = next_url?. trim ( ) ;
663+ if value. is_empty ( ) {
664+ return None ;
665+ }
666+ if !value. starts_with ( '/' ) || value. starts_with ( "//" ) {
667+ return None ;
668+ }
669+ Some ( value. to_string ( ) )
670+ }
0 commit comments