From ee710f2c160192fa118619ccacaf4a5a97d20ff6 Mon Sep 17 00:00:00 2001 From: TimothyBlynJacobs Date: Wed, 27 Jan 2021 19:05:02 +0000 Subject: [PATCH] App Passwords: Improve validation and sanitization of the application name. Application names are now required to be unique and cannot contain solely whitespace characters. Additionally, invalid characters are now stripped from the application name using `sanitize_text_field()`. Props Boniu91, hellofromTonya, engahmeds3ed, xkon, francina. Fixes #51941. Built from https://develop.svn.wordpress.org/trunk@50030 git-svn-id: http://core.svn.wordpress.org/trunk@49731 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-admin/js/application-passwords.js | 2 +- wp-admin/js/application-passwords.min.js | 2 +- wp-admin/js/auth-app.js | 2 +- wp-admin/js/auth-app.min.js | 2 +- .../class-wp-application-passwords.php | 37 ++++++++++++++++++- ...-rest-application-passwords-controller.php | 2 + wp-includes/version.php | 2 +- 7 files changed, 43 insertions(+), 6 deletions(-) diff --git a/wp-admin/js/application-passwords.js b/wp-admin/js/application-passwords.js index 7901ac901b..cab592519b 100644 --- a/wp-admin/js/application-passwords.js +++ b/wp-admin/js/application-passwords.js @@ -57,7 +57,7 @@ $newAppPassButton.prop( 'disabled', false ); $newAppPassForm.after( tmplNewAppPass( { - name: name, + name: response.name, password: response.password } ) ); $( '.new-application-password-notice' ).focus(); diff --git a/wp-admin/js/application-passwords.min.js b/wp-admin/js/application-passwords.min.js index 90b6dcb131..5d9fa5010e 100644 --- a/wp-admin/js/application-passwords.min.js +++ b/wp-admin/js/application-passwords.min.js @@ -1,2 +1,2 @@ /*! This file is auto-generated */ -!function(o){var a=o("#application-passwords-section"),i=a.find(".create-application-password"),n=i.find(".input"),t=i.find(".button"),p=a.find(".application-passwords-list-table-wrapper"),r=a.find("tbody"),d=r.find(".no-items"),e=o("#revoke-all-application-passwords"),l=wp.template("new-application-password"),c=wp.template("application-password-row"),u=o("#user_id").val();function w(e,s,a){f(e.responseJSON&&e.responseJSON.message?e.responseJSON.message:a,"error")}function f(e,s){e=o("
").attr("role","alert").attr("tabindex","-1").addClass("is-dismissible notice notice-"+s).append(o("

").text(e)).append(o("").attr("type","button").addClass("notice-dismiss").append(o("").addClass("screen-reader-text").text(wp.i18n.__("Dismiss this notice."))));return i.after(e),e}function v(){o(".notice",a).remove()}t.on("click",function(e){var s,a;e.preventDefault(),t.prop("aria-disabled")||(0!==(s=n.val()).length?(v(),t.prop("aria-disabled",!0).addClass("disabled"),a={name:s},a=wp.hooks.applyFilters("wp_application_passwords_new_password_request",a,u),wp.apiRequest({path:"/wp/v2/users/"+u+"/application-passwords?_locale=user",method:"POST",data:a}).always(function(){t.removeProp("aria-disabled").removeClass("disabled")}).done(function(e){n.val(""),t.prop("disabled",!1),i.after(l({name:s,password:e.password})),o(".new-application-password-notice").focus(),r.prepend(c(e)),p.show(),d.remove(),wp.hooks.doAction("wp_application_passwords_created_password",e,a)}).fail(w)):n.focus())}),r.on("click",".delete",function(e){var s,a;e.preventDefault(),window.confirm(wp.i18n.__("Are you sure you want to revoke this password? This action cannot be undone."))&&(s=o(this),e=(a=s.closest("tr")).data("uuid"),v(),s.prop("disabled",!0),wp.apiRequest({path:"/wp/v2/users/"+u+"/application-passwords/"+e+"?_locale=user",method:"DELETE"}).always(function(){s.prop("disabled",!1)}).done(function(e){e.deleted&&(0===a.siblings().length&&p.hide(),a.remove(),f(wp.i18n.__("Application password revoked."),"success").focus())}).fail(w))}),e.on("click",function(e){var s;e.preventDefault(),window.confirm(wp.i18n.__("Are you sure you want to revoke all passwords? This action cannot be undone."))&&(s=o(this),v(),s.prop("disabled",!0),wp.apiRequest({path:"/wp/v2/users/"+u+"/application-passwords?_locale=user",method:"DELETE"}).always(function(){s.prop("disabled",!1)}).done(function(e){e.deleted&&(r.children().remove(),a.children(".new-application-password").remove(),p.hide(),f(wp.i18n.__("All application passwords revoked."),"success").focus())}).fail(w))}),a.on("click",".notice-dismiss",function(e){e.preventDefault();var s=o(this).parent();s.removeAttr("role"),s.fadeTo(100,0,function(){s.slideUp(100,function(){s.remove(),n.focus()})})}),0===r.children("tr").not(d).length&&p.hide()}(jQuery); \ No newline at end of file +!function(o){var a=o("#application-passwords-section"),i=a.find(".create-application-password"),n=i.find(".input"),t=i.find(".button"),p=a.find(".application-passwords-list-table-wrapper"),r=a.find("tbody"),d=r.find(".no-items"),e=o("#revoke-all-application-passwords"),l=wp.template("new-application-password"),c=wp.template("application-password-row"),u=o("#user_id").val();function w(e,s,a){f(e.responseJSON&&e.responseJSON.message?e.responseJSON.message:a,"error")}function f(e,s){e=o("
").attr("role","alert").attr("tabindex","-1").addClass("is-dismissible notice notice-"+s).append(o("

").text(e)).append(o("").attr("type","button").addClass("notice-dismiss").append(o("").addClass("screen-reader-text").text(wp.i18n.__("Dismiss this notice."))));return i.after(e),e}function v(){o(".notice",a).remove()}t.on("click",function(e){var s;e.preventDefault(),t.prop("aria-disabled")||(0!==(e=n.val()).length?(v(),t.prop("aria-disabled",!0).addClass("disabled"),s={name:e},s=wp.hooks.applyFilters("wp_application_passwords_new_password_request",s,u),wp.apiRequest({path:"/wp/v2/users/"+u+"/application-passwords?_locale=user",method:"POST",data:s}).always(function(){t.removeProp("aria-disabled").removeClass("disabled")}).done(function(e){n.val(""),t.prop("disabled",!1),i.after(l({name:e.name,password:e.password})),o(".new-application-password-notice").focus(),r.prepend(c(e)),p.show(),d.remove(),wp.hooks.doAction("wp_application_passwords_created_password",e,s)}).fail(w)):n.focus())}),r.on("click",".delete",function(e){var s,a;e.preventDefault(),window.confirm(wp.i18n.__("Are you sure you want to revoke this password? This action cannot be undone."))&&(s=o(this),e=(a=s.closest("tr")).data("uuid"),v(),s.prop("disabled",!0),wp.apiRequest({path:"/wp/v2/users/"+u+"/application-passwords/"+e+"?_locale=user",method:"DELETE"}).always(function(){s.prop("disabled",!1)}).done(function(e){e.deleted&&(0===a.siblings().length&&p.hide(),a.remove(),f(wp.i18n.__("Application password revoked."),"success").focus())}).fail(w))}),e.on("click",function(e){var s;e.preventDefault(),window.confirm(wp.i18n.__("Are you sure you want to revoke all passwords? This action cannot be undone."))&&(s=o(this),v(),s.prop("disabled",!0),wp.apiRequest({path:"/wp/v2/users/"+u+"/application-passwords?_locale=user",method:"DELETE"}).always(function(){s.prop("disabled",!1)}).done(function(e){e.deleted&&(r.children().remove(),a.children(".new-application-password").remove(),p.hide(),f(wp.i18n.__("All application passwords revoked."),"success").focus())}).fail(w))}),a.on("click",".notice-dismiss",function(e){e.preventDefault();var s=o(this).parent();s.removeAttr("role"),s.fadeTo(100,0,function(){s.slideUp(100,function(){s.remove(),n.focus()})})}),0===r.children("tr").not(d).length&&p.hide()}(jQuery); \ No newline at end of file diff --git a/wp-admin/js/auth-app.js b/wp-admin/js/auth-app.js index b4b8ddbda5..f358dd7e64 100644 --- a/wp-admin/js/auth-app.js +++ b/wp-admin/js/auth-app.js @@ -98,7 +98,7 @@ .append( '

' + wp.i18n.__( 'Be sure to save this in a safe location. You will not be able to retrieve it.' ) + '

' ); // We're using .text() to write the variables to avoid any chance of XSS. - $( 'strong', $notice ).text( name ); + $( 'strong', $notice ).text( response.name ); $( 'input', $notice ).val( response.password ); $form.replaceWith( $notice ); diff --git a/wp-admin/js/auth-app.min.js b/wp-admin/js/auth-app.min.js index daacbeb0ed..062f3b31a5 100644 --- a/wp-admin/js/auth-app.min.js +++ b/wp-admin/js/auth-app.min.js @@ -1,2 +1,2 @@ /*! This file is auto-generated */ -!function(t,n){var s=t("#app_name"),r=t("#approve"),e=t("#reject"),i=s.closest("form"),o={userLogin:n.user_login,successUrl:n.success,rejectUrl:n.reject};r.click(function(e){var p=s.val(),a=t('input[name="app_id"]',i).val();e.preventDefault(),r.prop("aria-disabled")||(0!==p.length?(r.prop("aria-disabled",!0).addClass("disabled"),e={name:p},0'+wp.i18n.__("Your new password for %s is:")+"","")+' ',o=t("
").attr("role","alert").attr("tabindex",-1).addClass("notice notice-success notice-alt").append(t("

").addClass("application-password-display").html(o)).append("

"+wp.i18n.__("Be sure to save this in a safe location. You will not be able to retrieve it.")+"

"),t("strong",o).text(p),t("input",o).val(e.password),i.replaceWith(o),o.focus())}).fail(function(e,a,s){var o=s,p=null;e.responseJSON&&(p=e.responseJSON).message&&(o=p.message);o=t("
").attr("role","alert").addClass("notice notice-error").append(t("

").text(o));t("h1").after(o),r.removeProp("aria-disabled",!1).removeClass("disabled"),wp.hooks.doAction("wp_application_passwords_approve_app_request_error",p,a,s,e)})):s.focus())}),e.click(function(e){e.preventDefault(),wp.hooks.doAction("wp_application_passwords_reject_app",o),window.location=n.reject}),i.on("submit",function(e){e.preventDefault()})}(jQuery,authApp); \ No newline at end of file +!function(t,p){var o=t("#app_name"),n=t("#approve"),e=t("#reject"),r=o.closest("form"),i={userLogin:p.user_login,successUrl:p.success,rejectUrl:p.reject};n.click(function(e){var a=o.val(),s=t('input[name="app_id"]',r).val();e.preventDefault(),n.prop("aria-disabled")||(0!==a.length?(n.prop("aria-disabled",!0).addClass("disabled"),a={name:a},0'+wp.i18n.__("Your new password for %s is:")+"","")+' ',o=t("
").attr("role","alert").attr("tabindex",-1).addClass("notice notice-success notice-alt").append(t("

").addClass("application-password-display").html(o)).append("

"+wp.i18n.__("Be sure to save this in a safe location. You will not be able to retrieve it.")+"

"),t("strong",o).text(e.name),t("input",o).val(e.password),r.replaceWith(o),o.focus())}).fail(function(e,a,s){var o=s,p=null;e.responseJSON&&(p=e.responseJSON).message&&(o=p.message);o=t("
").attr("role","alert").addClass("notice notice-error").append(t("

").text(o));t("h1").after(o),n.removeProp("aria-disabled",!1).removeClass("disabled"),wp.hooks.doAction("wp_application_passwords_approve_app_request_error",p,a,s,e)})):o.focus())}),e.click(function(e){e.preventDefault(),wp.hooks.doAction("wp_application_passwords_reject_app",i),window.location=p.reject}),r.on("submit",function(e){e.preventDefault()})}(jQuery,authApp); \ No newline at end of file diff --git a/wp-includes/class-wp-application-passwords.php b/wp-includes/class-wp-application-passwords.php index 22ad1c72c8..0fd5d8b40a 100644 --- a/wp-includes/class-wp-application-passwords.php +++ b/wp-includes/class-wp-application-passwords.php @@ -58,6 +58,7 @@ class WP_Application_Passwords { * Creates a new application password. * * @since 5.6.0 + * @since 5.7.0 Returns WP_Error if application name already exists. * * @param int $user_id User ID. * @param array $args Information about the application password. @@ -65,8 +66,16 @@ class WP_Application_Passwords { * A WP_Error instance is returned on error. */ public static function create_new_application_password( $user_id, $args = array() ) { + if ( ! empty( $args['name'] ) ) { + $args['name'] = sanitize_text_field( $args['name'] ); + } + if ( empty( $args['name'] ) ) { - return new WP_Error( 'application_password_empty_name', __( 'An application name is required to create an application password.' ) ); + return new WP_Error( 'application_password_empty_name', __( 'An application name is required to create an application password.' ), array( 'status' => 400 ) ); + } + + if ( self::application_name_exists_for_user( $user_id, $args['name'] ) ) { + return new WP_Error( 'application_password_duplicate_name', __( 'Each application name should be unique.' ), array( 'status' => 409 ) ); } $new_password = wp_generate_password( static::PW_LENGTH, false ); @@ -162,6 +171,28 @@ class WP_Application_Passwords { return null; } + /** + * Check if application name exists before for this user. + * + * @since 5.7.0 + * + * @param int $user_id User ID. + * @param string $name Application name. + * + * @return bool Provided application name exists or not. + */ + public static function application_name_exists_for_user( $user_id, $name ) { + $passwords = static::get_user_application_passwords( $user_id ); + + foreach ( $passwords as $password ) { + if ( strtolower( $password['name'] ) === strtolower( $name ) ) { + return true; + } + } + + return false; + } + /** * Updates an application password. * @@ -180,6 +211,10 @@ class WP_Application_Passwords { continue; } + if ( ! empty( $update['name'] ) ) { + $update['name'] = sanitize_text_field( $update['name'] ); + } + $save = false; if ( ! empty( $update['name'] ) && $item['name'] !== $update['name'] ) { diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-application-passwords-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-application-passwords-controller.php index dc94348022..622d3617c2 100644 --- a/wp-includes/rest-api/endpoints/class-wp-rest-application-passwords-controller.php +++ b/wp-includes/rest-api/endpoints/class-wp-rest-application-passwords-controller.php @@ -631,6 +631,8 @@ class WP_REST_Application_Passwords_Controller extends WP_REST_Controller { 'type' => 'string', 'required' => true, 'context' => array( 'view', 'edit', 'embed' ), + 'minLength' => 1, + 'pattern' => '.*\S.*', ), 'password' => array( 'description' => __( 'The generated password. Only available after adding an application.' ), diff --git a/wp-includes/version.php b/wp-includes/version.php index 1ff22af295..88c23aa67a 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -13,7 +13,7 @@ * * @global string $wp_version */ -$wp_version = '5.7-alpha-50029'; +$wp_version = '5.7-alpha-50030'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.