19 January 2017

จัดการปัญหา WebView กับ SSL Certificate อย่างไรให้ถูกต้อง

Updated on

        ถึงแม้ว่าเจ้าของบล็อกจะไม่ค่อยชอบใช้ WebView ภายในแอปฯซักเท่าไร แต่ในบางครั้งก็เลี่ยงไม่ได้เพราะว่าฟีเจอร์บางตัวยังเป็นหน้าเว็ปอยู่ แต่มันดันเป็นหน้าเว็ป HTTPS ที่เจอปัญหา SSL Error นี่แหละ ดังนั้นต้องหาวิธีจัดการให้ถูกต้องแล้วล่ะ!!

เมื่อ WebView เจอปัญหา SSL Error

        เกิดขึ้นได้เป็นปกติเมื่อ URL ปลายทางนั้นมีปัญหาเกี่ยวกับ SSL ซึ่งหลักๆก็คงไม่พ้นเรื่อง Certificate มีปัญหานี่แหละ (Expired บ้าง, Self-Signed บ้าง และอื่นๆอีกมากมาย) ซึ่งผลลัพธ์ที่ได้จะเป็นแบบนี้


        หรือบางเวอร์ชันก็ขึ้นหน้าขาวโพลนไปเลย

        แต่บางครั้งฝั่งแอปฯก็ต้องหาทางแสดงผลให้ได้เสียก่อน (ถึงแม้ว่าวิธีแก้ไขที่ถูกต้องคือต้องแก้จากทางฝั่ง Server) เพราะขนาดเวลาเปิดบน Chrome ยังมีให้กด Proceed เพื่อยืนยันการเข้าสู่หน้าเว็ปเลย


เว็ปสำหรับทดสอบ SSL Error

        เนื่องจากจะต้องทดสอบกับ URL ซักแห่งที่มีปัญหา SSL Error ซึ่งจะไปหาตามหน้าเว็ปทั่วไปก็ใช่ว่าจะเจอกันได้ง่ายๆ จะให้สร้าง Server ขึ้นมาเทสเองก็ดูเหมือนจะเสียเวลาเกินไปหน่อย จนสุดท้ายเจ้าของบล็อกได้ไปเจอเว็ปไซต์แห่งหนึ่งที่มีไว้สำหรับทดสอบ SSL Error ทุกกรณีที่มีชื่อว่า https://badssl.com/



        อยากจะทดสอบกับ SSL Error แบบไหนก็กดเลือกแล้วเอา URL มาใช้ได้เลย โคตรสะดวกกกกกกก

วิธีแก้ปัญหาที่ไม่ถูกต้อง (แต่ก็ได้ผลเหมือนกัน)

        เมื่อเข้าหน้าเว็ปดังกล่าวไม่ได้ นักพัฒนาบางคนจึงใช้วิธีสั่ง Proceed ผ่านโค้ดของ WebViewClient แบบนี้

private WebView webView;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_web_view);

    webView = (WebView) findViewById(R.id.webView);

    ...

    webView.setWebViewClient(new CustomWebViewClient());
    webView.loadUrl("url_with_ssl_error");
}

public class CustomWebViewClient extends WebViewClient {
    @Override
    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
        handler.proceed();
    }
}

        เวลาที่เจอปัญหาเกี่ยวกับ SSL Error บน WebView จะสามารถสร้างคลาส WebViewClient แล้ว Override Method ที่ชื่อว่า onReceivedSslError เพื่อดัก Event ดังกล่าวได้ ดังนั้นผู้ที่หลงเข้ามาอ่านจึงสามารถสั่ง Proceed จากคลาส SslErrorHandler ที่มีอยู่ได้เลย

        เมื่อลองทดสอบใหม่อีกครั้ง


        เย้ เย้ เข้าได้แล้วววววววว

        แต่หลังจากที่แอปฯขึ้น Google Play Store ไปได้ซักพักหนึ่ง ผู้ที่หลงเข้ามาอ่านก็จะได้รับอีเมลล์จากทาง Google Play ที่แจ้งปัญหาเกี่ยวกับ Security Warning โดยมีใจความแบบนี้


        จริงๆข้อความจะยาวมาก แต่สรุปสั้นๆก็คือวิธีที่ใช้แก้ไขปัญหา SSL Error ใน WebView นั้นไม่ถูกต้อง เพราะการบังคับ Proceed ทุกครั้งแบบนี้จะทำให้แอปฯเกิดช่องโหว่ที่อาจจะถูกโจมตีด้วยวิธี Man-in-the-middle โดยที่ผู้ใช้ไม่สามารถหลีกเลี่ยงได้เลย ดังนั้นจึงควรแก้ไขซะ

วิธีแก้ปัญหาที่ถูกต้อง

        เมื่อเกิด URL ที่จะแสดงผลใน WebView เกิดปัญหา SSL Error จะต้องแสดง Dialog แจ้งเตือนแก่ผู้ใช้ว่า URL ที่กำลังจะเข้านั้นไม่ปลอดภัย แล้วให้ผู้ใช้เป็นคนตัดสินใจแทนว่าจะปิดหน้าดังกล่าวหรือว่าเข้าใช้งานต่อ

        SSL Error นั้นมีหลายสาเหตุ เพื่อให้แสดงข้อความตามประเภทของ SSL Error ผู้ที่หลงเข้ามาอ่าสามารถเช็คแล้วกำหนดข้อความตามที่ต้องการได้

private String getSslErrorMessage(SslError error) {
    switch (error.getPrimaryError()) {
        case SslError.SSL_DATE_INVALID:
            return "The certificate date is invalid.";
        case SslError.SSL_EXPIRED:
            return "The certificate has expired.";
        case SslError.SSL_IDMISMATCH:
            return "The certificate hostname mismatch.";
        case SslError.SSL_INVALID:
            return "The certificate is invalid.";
        case SslError.SSL_NOTYETVALID:
            return "The certificate is not yet valid";
        case SslError.SSL_UNTRUSTED:
            return "The certificate is untrusted.";
        default:
            return "SSL Certificate error.";
    }
}

        ซึ่ง getSslErrorMessage(SslError error) มีไว้แสดงข้อความตามประเภทของ Error เพื่อแสดงผล Dialog ให้ผู้ใช้รับรู้นั่นเอง

        ทีนี้ก็สร้าง Dialog ขึ้นมาเพื่อแจ้งให้ผู้ใช้รับรู้ โดยมีตัวเลือกระหว่าง Proceed กับ Cancel

...

public class CustomWebViewClient extends WebViewClient {
    @Override
    public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslError error) {
        String message = getSslErrorMessage(error);
        String proceed = "Proceed";
        String cancel = "Cancel";
        AlertDialog.Builder builder = new AlertDialog.Builder(view.getContext());
        builder.setMessage(message)
                .setPositiveButton(proceed, new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        handler.proceed();
                    }
                })
                .setNegativeButton(cancel, new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        handler.cancel();
                    }
                });
        builder.create().show();
    }

    private String getSslErrorMessage(SslError error) {

        ...

    }

    ...
}

        ดังนั้นเวลาที่ WebView โหลดหน้าเว็ปใดๆก็ตามแล้วเกิดปัญหา SSL Error ก็จะแสดง Dialog แจ้งเตือนผู้ใช้แบบนี้


        เพียงเท่านี้ผู้ใช้ก็สามารถเลือกได้ว่าจะปิด (Cancel) หรือเข้าใช้งานต่อ (Proceed)

สรุป

        ในกรณีที่มีการใช้งาน WebView แล้วต้องเข้าหน้าเว็ปที่มีปัญหา SSL Error ไม่ว่าจะกรณีใดก็ตาม ผู้ที่หลงเข้ามาอ่านไม่ควรสั่ง Proceed เพื่อเข้าหน้าเว็ปทันที แต่ควรจะแสดง Dialog แจ้งเตือนแล้วให้ผู้ใช้เป็นคนตัดสินใจเลือกเองว่าจะเข้าใช้งานต่อหรือว่าปิดหน้านั้นๆทิ้งไปซะ ถึงแม้ว่าวิธีดังกล่าวจะไม่ได้ช่วยป้องกันช่องโหว่ Man-in-the-middle ก็ตาม (แต่ก็ช่วยป้องกัน Security Alert จาก Google Play นะ) แต่ผู้ใช้ก็สามารถตัดสินใจเลือกได้ด้วยตัวเองเหมือนกับบนหน้าเว็ปของ Chrome

        ทางที่ดีที่สุดก็คือ แก้ไขจากฝั่ง Server ตั้งแต่แรกเถอะครับ...