Correct way to make meta box with more than one meta field secure

First, let me say that there is no web application 100% secure.

That being said, you are using the nonce correctly. The function you are using, update_post_meta(), will sql-scape the data as it uses insert/update methods of wpdb class. So, there is no risk for most common security problemas.

What you should take care, I think, is data validation, and you are doing that incorrectly. You pass all meta fields values to wp_kses() function and you allow <a> elements in all the fields. I think that is not what you want; for example, I think you don’t want to allow <a> element in the email or phone fields.

Instead of pass all values to wp_kses() you should do a specific data validation and/or sanitization for each one. For example:

if( isset( $_POST['products_sold'] ) ) {
    update_post_meta( $post_id, 'products_sold', sanitize_text_field( $_POST['products_sold'] ) );
}
if( isset( $_POST['website'] ) ) {
     //Leave as it was to allow website as <a href="https://wordpress.stackexchange.com/questions/155143/...">...</a>
     //update_post_meta( $post_id, 'website', wp_kses( $_POST['website'], $allowed ) );
     update_post_meta( $post_id, 'website', esc_url_raw( $_POST['website'] ) );
}

if( isset( $_POST['address'] ) ) {
    update_post_meta( $post_id, 'address', sanitize_text_field( $_POST['address'] ) );
}

if( isset( $_POST['phone'] ) ) {
     update_post_meta( $post_id, 'phone', sanitize_text_field( $_POST['phone'] ) );
}

if( isset( $_POST['email'] ) ) {
    update_post_meta( $post_id, 'email', sanitize_email( $_POST['email'] ) );
}

if( isset( $_POST['hours'] ) ) {
    update_post_meta( $post_id, 'hours', sanitize_text_field( $_POST['hours'] ) );
}

You could go further and define a custom validation function that will be performed every time a specific meta field is updated/created. For example:

//Create and define the `sanitize_phone_meta` filter:
add_filter( 'sanitize_post_meta_phone', 'sanitize_phone_meta' );
function ssanitize_phone_meta( $phone ) {

    //Perform whatever validation you want for phone value
    //For example, if you only want the phone format 000-0000-0000
    if(preg_match("/^[0-9]{3}-[0-9]{4}-[0-9]{4}$/", $phone)) {
        // $phone is valid
        return $phone;

    } else {
        return false;
    }

}

Then, in the save_post hook:

if( isset( $_POST['phone'] ) ) {
     $clean_phone = sanitize_meta( 'phone', $_POST['phone'], 'post' );
     if( $clean_phone !== false ) {
         update_post_meta( $post_id, 'phone', $clean_phone );
     } else {
         //Do something when the phone format is not what you expect
     }
}

This way, a little more work is required, but the phone meta field will be sanitized against the defined filter every time the phone meta is created/updated from any where without the need of writing the sanitzation code again.

More in: