Author: grianggrai.n

  • WordPress Content Security Policy (2)

    WordPress Content Security Policy (2)

    ครั้งก่อนนำเสนอปลั๊กอิน CSP-ANTS&STP ซึ่งปัจจุบันได้เปลี่ยนชื่อเป็น CSP Friendly Security แต่ถึงจะมีการปรับปรุงแล้วก็อาจเกิด Header Overflow ได้ในอนาคต ดูผลลัพธ์ของ CSP ที่ได้เป็นดังภาพ

    ซึ่งหลังจากคุยกับ Gemini ก็ได้รับคำแนะนำให้เปลี่ยน เนื่องจากมีโอกาสเกิด Header Overflow ได้ (ถึงแม้จะมีขนาดใหญ่มาก 8KB ถึง 16KB) ก็เลยให้เขียนใหม่ปรับปรุงจากรุ่นเดิมของ CSP Friendly Security โดยใช้ z.ai และ gemini.google.com ปรับปรุง

    สิ่งที่ต้องมี

    1. Woody snippets
    2. สร้าง new php snippets ใส่โค้ดต่อไปนี้แล้วเปลี่ยนเป็น run everywhere แล้วกดบันทึกให้เรียบร้อย
    CSP {PHP}
    add_action('template_redirect', 'csp_controller');
    
    function csp_controller() {
        // ตรวจสอบว่าเป็นหน้า Admin หรือหน้าเป้าหมายหรือไม่
        $target_pages = array('cd-key');
        if (is_admin() || is_page($target_pages)) {
            // ส่ง Simple Header แล้วจบการทำงานทันที (ไม่เปิด Buffer)
            if (!headers_sent()) {
                header("Content-Security-Policy: upgrade-insecure-requests");
            }
            return;
        }
    
        if (function_exists('litespeed_autoload')) {
            add_filter('litespeed_buffer_after', 'process_csp_buffer', 0);
        } else {
            ob_start('process_csp_buffer');
        }
    }
    
    function process_csp_buffer($content) {
        // ตรวจสอบว่าเป็นหน้า Admin หรือหน้าเป้าหมายหรือไม่
        $target_pages = array('cd-key');
        if (is_admin() || is_page($target_pages)) {
            return $content;
        }
    
        // สร้าง Nonce
        $nonce = base64_encode(random_bytes(16));
    
        // แทรก Nonce (ใช้รหัส Hex เพื่อหลบ Editor บั๊ก)
        $pattern = '#<(script|style)(?![^>]*\bnonce=)(?![^>]*\btype=[\x22\x27]application/(ld\+json|json)[\x22\x27])([^>]*)>#i';
        $content = preg_replace($pattern, "<$1 nonce='" . $nonce . "'$3>", $content);
    
        $sha256_csp = get_event_hashes($content);
        $uris = get_allowed_domains($content);
        $uri_string = implode(' ', $uris);
    
        $script_src = "script-src https: 'strict-dynamic' 'nonce-" . $nonce . "'";
        if (!empty($sha256_csp)) {
            $script_src .= " " . $sha256_csp;
        }
    
        // สร้าง Full CSP Header
        $csp_header = sprintf(
            "Content-Security-Policy: base-uri 'self' %s data:; object-src 'none'; %s; frame-ancestors 'none';",
            $uri_string,
            $script_src
        );
    
        if (!headers_sent()) {
            header($csp_header);
        }
    
        return $content;
    }
    
    // --- ฟังก์ชัน Helper (คงเดิม) ---
    
    function get_event_hashes($output) {
        $sha256 = array();
        if (preg_match_all('#\s(onload|onclick)\s*=\s*([\x22\x27])(.+?)\2#is', $output, $matches)) {
            foreach ($matches[3] as $event_code) {
                if (!empty($event_code)) {
                    $sha256[] = base64_encode(hash('sha256', $event_code, true));
                }
            }
        }
        if (class_exists('autoptimizeConfig')) {
            $sha256[] = base64_encode(hash('sha256', "this.onload=null;this.media='all';", true));
        }
        if (empty($sha256)) return '';
        $unique_hashes = array_unique($sha256);
        $formatted_hashes = array();
        foreach ($unique_hashes as $h) {
            $formatted_hashes[] = "'sha256-" . $h . "'";
        }
        return "'unsafe-hashes' " . implode(' ', $formatted_hashes);
    }
    
    function get_allowed_domains($string) {
        $result = array();
        $domains = array(
            'https://secure.gravatar.com/avatar/',
            'https://fonts.googleapis.com/',
            'https://maxcdn.bootstrapcdn.com/',
            'https://cdn.jsdelivr.net/'
        );
        foreach ($domains as $domain) {
            if (strpos($string, $domain) !== false) {
                $result[] = $domain;
            }
        }
        return $result;
    }
    1. เมื่อไปทดสอบที่ https://securityheaders.com/
    2. ก็จะได้ CSP ใหม่ที่สั้นลง
    content-security-policybase-uri ‘self’ data:; object-src ‘none’; script-src https: ‘strict-dynamic’ ‘nonce-sdfadsdfasdfadsfadsfa==’; frame-ancestors ‘none’;
    1. ก็ยังมี recomendation ที่ต้องปรับปรุงอีกนิดหน่อยค่อยๆ ทำไป
    2. จบ… ขอให้สนุก
  • Subresource Integrity

    Subresource Integrity

    เมื่อต้องการเพิ่มคะแนนของ HTTP Observatory Report เลื่อนไปๆ จะเจอว่าคะแนนของ Subresource integrity (SRI) ยังเป็น -5 ทำไงได้บ้างสำหรับ WordPress


    SRI คืออะไร

    SRI หรือ Subresource Integrity คือฟีเจอร์ด้านความปลอดภัยที่ช่วยให้ Browser ตรวจสอบว่าไฟล์ที่ถูกดึงมาจากภายนอก (เช่น JS หรือ CSS จาก CDN) ไม่ถูกแก้ไขหรือแอบฝังมัลแวร์มาโดยผู้ไม่หวังดี

    สิ่งที่ต้องมี

    1. ปลักอิน Woody snippets
    2. code สร้าง SRI ซึ่ง code นี้ได้จาก ai 2 ตัวช่วยกันคิด (กรั่ก ๆ) คือ z.ai และ gemini.google.com
    PHP
    // 1. Hook เพื่อแทรก SRI ให้ Script และ Style
    add_filter( 'script_loader_tag', 'auto_add_sri', 10, 2 );
    add_filter( 'style_loader_tag', 'auto_add_sri', 10, 2 );
    
    function auto_add_sri( $tag, $handle ) {
        if ( ! preg_match( '/\s(src|href)=[\x27\x22]([^\x27\x22]+)[\x27\x22]/i', $tag, $matches ) ) {
            return $tag;
        }
        
        $attr = $matches[1]; 
        $url  = $matches[2];
    
        $site_url = site_url();
        if ( strpos( $url, 'http' ) !== 0 || strpos( $url, $site_url ) === 0 ) {
            return $tag;
        }
    
        $hash_key = 'sri_hash_' . md5( $url );
        $hash = get_transient( $hash_key );
    
        if ( false === $hash ) {
            $response = wp_remote_get( $url, array( 'timeout' => 5 ) ); 
            
            if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) != 200 ) {
                set_transient( $hash_key, 'failed', HOUR_IN_SECONDS );
                return $tag;
            }
    
            $content = wp_remote_retrieve_body( $response );
            $hash = 'sha384-' . base64_encode( hash( 'sha384', $content, true ) );
            
            set_transient( $hash_key, $hash, 12 * HOUR_IN_SECONDS );
        }
    
        if ( $hash && $hash !== 'failed' ) {
            $replace_target = ' ' . $attr . '=';
            $replace_with = ' integrity="' . $hash . '" crossorigin="anonymous" ' . $attr . '=';
            return str_replace( $replace_target, $replace_with, $tag );
        }
    
        return $tag;
    }
    1. เปิดใช้งาน code ให้เป็น run everywhere
    2. ลองไปทดสอบที่เว็บ https://developer.mozilla.org/en-US/observatory
    3. ได้ผลดังภาพ
    1. ยังมีคะแนนที่ต้องมีการปรับตาม recomendation เพิ่มเติม
    2. สคริปต์นี้จะเก็บ cache ของ integrity ของสคริปต์ภายนอกไว้ 12 ชั่วโมง ถ้ามีผู้ใช้เข้าเว็บในช่วงเวลานี้แล้ว สคริปต์ไม่มีการเปลี่ยนแปลงก็จะไม่มีปัญหา แต่ถ้ามีการเปลี่ยนแปลงแล้วค่า Hash ไม่ตรงก็จะโดนบล็อคทันที ซึ่งต้องดูว่าเวลาเท่าไหร่จึงจะเหมาะสม
    3. จบ…ขอให้สนุก
  • Debian Oval Security auto check

    Debian Oval Security auto check

    วันก่อนเขียนสคริปต์ตรวจสอบ Debian oval อัตโนมัติเขียนเสร็จ อ่ะ ไหนๆ สมัครโปรของ เจ๊มินิ (gemini.google.com) แล้ว ให้เจ๊ช่วยขัดเกลาสคริปต์ bash script บ้านๆ เสร็จปุบ ดูหรูไฮ… ทันตาเห็น จากสคริปต์ พบว่าเขียน bash ยังไงให้ถูก ? โวะเพิ่งรู้ว่ามีนั่นนี่โน่น…

    • สร้างสคริปต์ gem-oval-check.sh มีข้อความว่า
    gem-oval-check.sh
    #!/usr/bin/env bash
    # OVAL Updater & Parser - Final Fixed Version
    # Description: Downloads Debian OVAL definitions, runs check, and sends an email report.
    
    # -------------------------
    # 1. Configuration & Variables
    # -------------------------
    readonly DEBIAN_CODENAME=$(lsb_release -c | awk '{print $2}')
    readonly DEBIAN_RELEASE=$(cat /etc/debian_version)
    readonly SERVER_HOSTNAME=$(hostname -f)
    readonly SERVER_IP=$(hostname -I | awk '{print $1}')
    
    readonly URL="https://www.debian.org/security/oval/oval-definitions-${DEBIAN_CODENAME}.xml.bz2"
    readonly DEST_BZ2="oval-definitions-${DEBIAN_CODENAME}.xml.bz2"
    readonly DEST_XML="oval-definitions-${DEBIAN_CODENAME}.xml"
    readonly TMP_BZ2="/tmp/${DEST_BZ2}"
    readonly RESULT_XML="oval_result.xml"
    readonly ID_FILE="/tmp/oval_ids_${DEBIAN_CODENAME}"
    readonly MSG_FILE="/tmp/oval_messages_${DEBIAN_CODENAME}"
    readonly CVE_REPORT="/tmp/oval_cve_titles_parsed_${DEBIAN_CODENAME}"
    
    readonly REPORT_TO="***********Your E-mail***********"
    SUBJECT="OVAL Report ${SERVER_HOSTNAME} $(date +%F)"
    
    OVAL_TIMESTAMP="N/A"
    OVAL_VERSION="N/A"
    
    # -------------------------
    # 2. Functions
    # -------------------------
    
    # Function for clean up temporary files
    cleanup() {
        rm -f "${ID_FILE}" "${MSG_FILE}" "${CVE_REPORT}"
    }
    trap cleanup EXIT
    
    # Function to generate URL based on the security ID (CVE or DSA)
    generate_url() {
        local id_string=$1
        if [[ "$id_string" =~ ^CVE-[0-9]{4}-[0-9]+$ ]]; then
            echo "https://cve.mitre.org/cgi-bin/cvename.cgi?name=${id_string}"
        elif [[ "$id_string" =~ ^DSA-[0-9]+-[0-9]+$ ]]; then
            echo "https://www.debian.org/security/${id_string}"
        else
            echo "(No Link Available)"
        fi
    }
    
    # Function to generate and send email
    send_report() {
        local exit_status=$1
        local message_type=$2 # 'SUCCESS', 'INFO', 'ERROR', 'CVE_FOUND'
        
        local status_prefix=""
        case "${message_type}" in
            "SUCCESS")
                status_prefix="[OVAL OK] Not Found"
                ;;
            "INFO")
                status_prefix="[OVAL INFO] Not Found (Old File Check)"
                ;;
            "ERROR")
                status_prefix="[OVAL ERROR]"
                ;;
            "CVE_FOUND")
                status_prefix="[OVAL ALERT] CVE Found!"
                ;;
        esac
        
        local final_subject="${status_prefix} - OVAL Report ${SERVER_HOSTNAME} $(date +%F)"
    
        {
            echo "Host Name: ${SERVER_HOSTNAME}"
            echo "IP Address: ${SERVER_IP}"
            echo "-------------------------------------"
            echo "OVAL Version: ${OVAL_VERSION}"
            echo "OVAL Timestamp: ${OVAL_TIMESTAMP}"
            echo "-------------------------------------"
        } > "${MSG_FILE}"
    
        case "${message_type}" in
            "SUCCESS")
                echo "**********************" >> "${MSG_FILE}"
                echo "* Congratulations!  *" >> "${MSG_FILE}"
                echo "**********************" >> "${MSG_FILE}"
                echo "Distribution: ${DEBIAN_CODENAME} Release: ${DEBIAN_RELEASE}" >> "${MSG_FILE}"
                echo "No CVE found" >> "${MSG_FILE}"
                echo "No definitions with result=true found" >> "${MSG_FILE}"
                ;;
            "INFO")
                echo "**********************" >> "${MSG_FILE}"
                echo "* OVAL Check Ran   *" >> "${MSG_FILE}"
                echo "**********************" >> "${MSG_FILE}"
                echo "File not modified (HTTP 304). Check performed on local file." >> "${MSG_FILE}"
                echo "Distribution: ${DEBIAN_CODENAME} Release: ${DEBIAN_RELEASE}" >> "${MSG_FILE}"
                echo "No CVE found" >> "${MSG_FILE}"
                ;;
            "ERROR")
                echo "**********************" >> "${MSG_FILE}"
                echo "* ERROR!       *" >> "${MSG_FILE}"
                echo "**********************" >> "${MSG_FILE}"
                echo "$3" >> "${MSG_FILE}"
                ;;
            "CVE_FOUND")
                echo "**********************" >> "${MSG_FILE}"
                echo "* CVE Found!!!   *" >> "${MSG_FILE}"
                echo "**********************" >> "${MSG_FILE}"
                echo "Distribution: ${DEBIAN_CODENAME} Release: ${DEBIAN_RELEASE}" >> "${MSG_FILE}"
                echo "--------------------------------------------------------" >> "${MSG_FILE}"
                echo "Package | Security ID | URL" >> "${MSG_FILE}"
                echo "--------------------------------------------------------" >> "${MSG_FILE}"
                cat "${CVE_REPORT}" >> "${MSG_FILE}" 
                ;;
        esac
    
        if ! mail -s "${final_subject}" "${REPORT_TO}" < "${MSG_FILE}"; then
            echo "🚨 ERROR: Failed to send email report to ${REPORT_TO}" >&2
        fi
        echo "Report sent to ${REPORT_TO}. Exiting with status ${exit_status}."
        exit "${exit_status}"
    }
    
    # -------------------------
    # 3. Main Logic
    # -------------------------
    
    echo "🚀 Starting OVAL check for Debian ${DEBIAN_CODENAME} (${DEBIAN_RELEASE}) on ${SERVER_HOSTNAME} (${SERVER_IP})..."
    
    STATUS=$(curl -s -f -w "%{http_code}" -z "${TMP_BZ2}" -o "${TMP_BZ2}" "${URL}" || echo "999")
    
    case "${STATUS}" in
        200)
            echo "✅ New/updated file downloaded (HTTP 200). Processing..."
            
            rm -f "${DEST_XML}" "${RESULT_XML}"
            cp "${TMP_BZ2}" "${DEST_BZ2}"
            
            ;&
    
        304)
            if [ "$STATUS" -eq 304 ]; then
                echo "ℹ️ OVAL file not modified (HTTP 304). Running check on existing file..."
            fi
            
            if [ ! -f "${DEST_XML}" ] && [ ! -f "${DEST_BZ2}" ]; then
                if [ -f "${TMP_BZ2}" ]; then
                    echo "⚠️ Local file missing. Using cached file for check."
                    cp "${TMP_BZ2}" "${DEST_BZ2}"
                else
                    send_report 1 "ERROR" "Cannot process OVAL check. Local files missing (HTTP ${STATUS})."
                fi
            fi
    
            if [ ! -f "${DEST_XML}" ]; then
                if ! bunzip2 -f "${DEST_BZ2}"; then
                    send_report 1 "ERROR" "Failed to decompress OVAL file: bunzip2 failed."
                fi
            fi
    
            echo "🔬 Running OVAL evaluation with oscap..."
            oscap oval eval --results "${RESULT_XML}" "${DEST_XML}" || true 
    
            if [ -f "${RESULT_XML}" ]; then
                OVAL_VERSION=$(grep 'oval:schema_version' "${DEST_XML}" 2>/dev/null | cut -d\> -f2 | cut -d\< -f1 | head -1 || echo "N/A")
                OVAL_TIMESTAMP=$(grep 'oval:timestamp' "${DEST_XML}" 2>/dev/null | cut -d\> -f2 | cut -d\< -f1 | head -1 || echo "N/A")
    
                echo "🔎 Extracted OVAL Version: ${OVAL_VERSION}, Timestamp: ${OVAL_TIMESTAMP} from result.xml"
            fi
            # ----------------------------------------------------------------------
    
            grep "<definition " "${RESULT_XML}" 2>/dev/null | \
              sed -nE 's/.*id="([^"]+)".*result="([^"]+)".*/\1,\2/p' | \
              awk -F, 'tolower($2) ~ /true/ {print $1}' | sort -u > "${ID_FILE}"
            
            if [ -s "${ID_FILE}" ]; then
                echo "🚨 Found $(wc -l < "${ID_FILE}") definitions with result=true."
                
                : > "${CVE_REPORT}" 
                
                while IFS= read -r ID || [ -n "${ID}" ]; do
                    [ -z "${ID}" ] && continue
                    
                    CVE_TITLE=$(grep -a -A6 -F -- "$ID" "${RESULT_XML}" 2>/dev/null | \
                                sed -n 's/.*<title>\(.*\)<\/title>.*/\1/p' | head -n1 || true)
    
                    if [ -n "$CVE_TITLE" ]; then
                        SECURITY_ID=$(echo "${CVE_TITLE}" | awk '{print $1}')
                        PACKAGE_NAME=$(echo "${CVE_TITLE}" | awk '{print $2}')
                        
                        LINK=$(generate_url "${SECURITY_ID}")
                        
                        PARSED_CVE="${PACKAGE_NAME} | ${SECURITY_ID} | ${LINK}"
                    else
                        PARSED_CVE="(no-title-found-for-${ID})"
                    fi
                    
                    echo "${PARSED_CVE}" >> "${CVE_REPORT}"
                done < "${ID_FILE}"
    
                send_report 0 "CVE_FOUND"
    
            else
                echo "✅ No vulnerable definitions found (result=true)."
                if [ "$STATUS" -eq 304 ]; then
                     send_report 0 "INFO"
                else
                     send_report 0 "SUCCESS"
                fi
            fi
            ;;
    
        999)
            send_report 1 "ERROR" "Network/Curl failed to reach ${URL}"
            ;;
    
        *)
            send_report 1 "ERROR" "Something went wrong: Unexpected HTTP status code ${STATUS} from ${URL}"
            ;;
    esac
    • เปลี่ยนสิทธิ์
    Bash
    chmod +x gem-oval-check.sh
    • อย่าลืมก่อนใช้งานต้องติดตั้ง openscap-scanner ด้วยคำสั่ง
    Bash
    apt install -y openscap-scanner
    • จากนั้นตั้ง crontab รันวันละครั้ง
    Bash
    crontab -e
    • กรอกข้อมูล
    Bash
    30 2 * * * /path_to_script/gem-oval-check.sh > /dev/null 2>&1
    • ก็จะมีอีเมลส่งมาทุกวันนนน
    • จบ
    • ขอให้สนุก
  • WordPress Content Security Policy

    WordPress Content Security Policy

    1. CSP สำหรับ WordPress ค่อนข้างยุ่งยาก ปรับโน่นนิดปิดนี่หน่อย เว็บเพี้ยน
    2. เมื่อลองค้นไป ก็เจอปลักอินอยู่ตัวนึง ที่ช่วยสร้าง CSP ให้อัตโนมัติ นั่นคือ CSP-ANTS&STP แต่เนื่องจากปลักอินตัวนี้ไม่ได้รับการปรับปรุงมาตั้งแต่ 2022 ซึ่งนานเกินไป แล้ว แต่จริงๆ มันยังใช้ได้อยู่
    3. แต่ปลั๊กอินก็ยังมีบักเมื่อไปตรวจกับเว็บ https://securityheaders.com แล้วจะมีข้อความเตือนดังภาพ
    1. เพื่อแก้ปัญหานี้เลยต้องเขียนปลักอินตัวนี้ลงในปลักอินอีกตัวที่ชื่อ Woody snippets
    2. คลิก Add ใน Woody snippets เพิ่มข้อความต่อไปนี้ ส่วนนี้ได้แก้ไข Error บน https://securityheaders.com แล้วด้วย
    /**
    * Plugin Name:       CSP-ANTS&ST
    * Description:       Add a nonce to each script and style tags, and set those nonces in CSP header
    * Version:           1.1.1
    * Requires at least: 5.9
    * Requires PHP:      7.3
    * Author:            Pascal CESCATO
    * Author URI:        https://pascalcescato.gdn/
    * License:           GPL v2 or later
    * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
    */
    
    add_action('send_headers', function () {
    if (is_admin()){
        header("Content-Security-Policy: upgrade-insecure-requests");
        return;
    } 
    //เลือก page ที่ไม่ต้องการให้ CSP ทำงาน
    if (is_page(['cd-key'])){
        header("Content-Security-Policy: upgrade-insecure-requests");
        return;
    } 
    
    if (function_exists('litespeed_autoload')):
    // lscache version
    function cspantsst_cspantsst_lscwp_check ( $content ) {
    
    $uris = implode ( ' ', cspantsst_search_for_sources ( $content ) );
    
    $sha256_csp = cspantsst_search_for_events ( $content );
    
    $nonces = [];
    
    $content = preg_replace_callback ( '#<script.*?\>#', function ( $matches ) use ( &$nonces ) {
    $nonce = wp_create_nonce ( $matches[ 0 ] );
    $nonces[] = $nonce;
    
    return str_replace ( '<script', "<script nonce='{$nonce}'", $matches[ 0 ] );
    }, $content );
    
    $content = preg_replace_callback ( '#<style.*?\>#', function ( $matches ) use ( &$nonces ) {
    $nonce = wp_create_nonce ( $matches[ 0 ] );
    $nonces[] = $nonce;
    
    return str_replace ( '<style', "<style nonce='{$nonce}'", $matches[ 0 ] );
    }, $content );
    
    $nonces_csp = array_reduce ( $nonces, function ( $header, $nonce ) {
    return "{$header} 'nonce-{$nonce}'";
    }, '' );
    
    header ( sprintf ( "Content-Security-Policy:  base-uri 'self' %1s data:; object-src 'none'; script-src https:%2s %3s 'strict-dynamic'; frame-ancestors 'none'; ", $uris, $nonces_csp, $sha256_csp ));
    return $content;
    }
    
    add_filter ( 'litespeed_buffer_after', 'cspantsst_cspantsst_lscwp_check', 0 );
    
    else:
    // otherwise
    add_action ( 'template_redirect', function () {
    
    ob_start ( function ( $output ) {
    
    $uris = implode ( ' ', cspantsst_search_for_sources ( $output ) );
    
    $sha256_csp = cspantsst_search_for_events ( $output );
    
    $nonces = [];
    
    $output = preg_replace_callback ( '#<script.*?\>#', function ( $matches ) use ( &$nonces ) {
    $nonce = wp_create_nonce ( $matches[ 0 ] );
    $nonces[] = $nonce;
    return str_replace ( '<script', "<script nonce='{$nonce}'", $matches[ 0 ] );
    }, $output );
    
    $output = preg_replace_callback ( '#<style.*?\>#', function ( $matches ) use ( &$nonces ) {
    $nonce = wp_create_nonce ( $matches[ 0 ] );
    $nonces[] = $nonce;
    return str_replace ( '<style', "<style nonce='{$nonce}'", $matches[ 0 ] );
    }, $output );
    
    $header = '';
    $nonces_csp = array_reduce ( $nonces, function ( $header, $nonce ) {
    return "{$header} 'nonce-{$nonce}'";
    }, '' );
    
    header ( sprintf ( "Content-Security-Policy: base-uri 'self' %1s data:; object-src 'none'; script-src https:%2s %3s 'strict-dynamic'; frame-ancestors 'none';", $uris, $nonces_csp, $sha256_csp ) );
    return $output;
    } );
    } );
    endif;
    
    function cspantsst_search_for_events ( $output ) {
    
    $sha256 = array ();
    
    preg_match_all ( '/onload="(?<onload>[^"]+)"|onclick="(?<onclick>[^"]+)"/', $output, $matches );
    foreach ( $matches[ 'onload' ] as $match ):
    if ( !empty ( $match ) )
    $sha256[] = base64_encode ( hash ( 'sha256', $match, true ) );
    endforeach;
    foreach ( $matches[ 'onclick' ] as $match ):
    if ( !empty ( $match ) )
    $sha256[] = base64_encode ( hash ( 'sha256', $match, true ) );
    endforeach;
    
    if ( class_exists ( 'autoptimizeConfig' ) ):
    $sha256[] = base64_encode ( hash ( 'sha256', "this.onload=null;this.media='all';", true ) );
    endif;
    
    
    $header_sha256 = "'unsafe-hashes'";
    $sha256_csp = array_reduce ( $sha256, function ( $header, $sha256_item ) {
    return "{$header} 'sha256-{$sha256_item}'";
    }, '' );
    
    if ( !empty ( $sha256_csp ) )
    $sha256_csp = $header_sha256 . $sha256_csp;
    
    return $sha256_csp;
    }
    
    function cspantsst_search_for_sources ( $string ) {
    
    $result = array ();
    if ( strpos ( $string, 'https://secure.gravatar.com/avatar/' ) ):
    $result[] = 'https://secure.gravatar.com/avatar/';
    endif;
    if ( strpos ( $string, 'https://fonts.googleapis.com/' ) ):
    $result[] = 'https://fonts.googleapis.com/';
    endif;
    if ( strpos ( $string, 'https://maxcdn.bootstrapcdn.com/' ) ):
    $result[] = 'https://maxcdn.bootstrapcdn.com/';
    endif;
    return $result;
    
    }
    });
    1. และใน code นี้มีส่วนของโค้ดที่ใช้ระบุหน้าที่ไม่ต้องการให้ CSP-ANTS&STP ทำงานด้วย เนื่องจากการกำหนด CSP บางครั้งอาจทำให้เว็บไม่สามารถทำงานได้อย่างที่ต้องการ ต้องปรับลดความปลอดภัยลงให้เหลือ upgrade-insecure-requests แทนโค้ดดังกล่าวคือ
    if (is_page(['cd-key'])){
        header("Content-Security-Policy: upgrade-insecure-requests");
        return;
    } 
    
    1. แล้วเลื่อนลงมาในส่วนของ Base options เลือกเป็น Run everywhere
    1. กด Save
    2. และมาตรวจสอบในหน้า Woody snippets ว่าเปิดใช้งานดังภาพแล้วหรือยัง
    1. ทดสอบด้วยการไปเช็คด้วย https://securityheaders.com/ ก็จะได้ A+ เย่..
    1. แถม https://developer.mozilla.org/en-US/observatory/ อีกเว็บนึง เมื่อติดตั้งโค้ดของ CSP ก็สามารถทำคะแนนที่ดีขึ้นทันตาเห็น
    1. สุดท้ายก็สามารถ ถอนปลักอิน CSP-ANTS&STP ออกจากเว็บไซต์ได้เลย ต้องขอบคุณคนเขียนปลักอินตัวนี้จากใจจริง
    2. จบ.
    3. ขอให้สนุก..
  • ส่งอีเมลในนามชื่อกลุ่มเมล์ บน Outlook

    ส่งอีเมลในนามชื่อกลุ่มเมล์ บน Outlook

    มีกลุ่มเมล์ ที่ใช้รับอีเมลจากภายนอก แต่เวลาให้เขาตอบกลับดันส่งมาอีเมลส่วนตัว เนื่องจากไม่ได้ใช้ชื่ออีเมลกลุ่ม ในการส่งเมล์ยังไงล่ะ เริ่มต้น

    1. ต้องแจ้งผุ้ดูแลระบบเพื่อขออนุญาติส่งอีเมลออกในชื่อกลุ่มเมล์ที่ดูแล ติดต่อที่ itoc
    2. ต้องใช้เว็บ https://outlook.office.com ในการส่งอีเมลเท่านั้น ไม่สามารถใช้ Gmail ได้
    3. เริ่มจากล็อคอินเข้า https://outlook.office.com ให้เรียบร้อยจะได้ดังภาพ
    1. คลิกที่เฟืองที่มุมบนขวาเพื่อเปิด Setting ขึ้นมาได้ดังภาพ
    1. เลือก Compose and reply
    1. ติ๊กถูกเลือก Always show From แล้วคลิก Save
    1. เมื่อเรากด New mail จะได้ดังภาพ จุดสังเกตคือมี. From ที่หลังปุ่ม Send
    1. ให้คลิกที่ From จะได้ดังภาพ แล้วให้คลิก Other email address…
    1. พิมพ์ชื่อกลุ่มเมล์ที่เราได้รับอนุญาติให้ส่งอีเมลได้แล้วลงไป แล้วเลือกดังภาพ
    1. ได้ดังภาพ
    1. ก็จะสามารถส่งอีเมลออกในชื่อกลุ่มเมล์ได้แล้ว
    2. จบขอให้สนุก…

  • Passkey and security keys in Google

    Passkey and security keys in Google

    เพื่อการเข้าใช้งานที่ง่ายขึ้นไปอีกขั้นแต่ปลอดภัยอยู้ (เสียงสูง)​​ Passkey เป็นการยืนยันตัวตนสองขั้นตอนอีกวิธีที่ใช้เพิ่มความปลอดภัยให้กับการล็อคอินเข้าใช้งาน มาตั้งค่าเพิ่มเติมกันสำหรับ Google

    1. เริ่มต้นล็อคอินเข้าระบบที่ https://myaccount.google.com/
    2. เมื่อล็อคอินเข้าระบบเรียบร้อยให้คลิกที่ Security แล้วเลื่อนลงมาที่ How you sign in to Google
    1. คลิกที่ 2-Step Verification จะได้ดังภาพ
    1. ในส่วนของ Second steps คลิกที่ Passkeys and security keys ได้ผลดังภาพ
    1. คลิก Create a passkey จะมีหน้าต่างหรือป็อบอัพแสดงขึ้นมาเพื่อให้เราใส่รหัสผ่านของเครื่องคอมพิวเตอร์ หรือมือถือที่ใช้เปิดหน้านี้ขึ้นมา อย่างในตัวอย่างนี้เปิดในเครื่องที่ใช้ระบบสแกนลายนิ้วมือในการเข้าใช้งาน ก็ให้สแกนลายนิ้วมือ เพื่อสร้าง Passkey เพียงเท่านี้ก็เสร็จ
    1. จะได้ดังรูป
    1. คลิก Try it out เพื่อทดสอบใช้งาาน ก็จะมีป็อบอัพให้กรอกรหัสผ่านเข้าเครื่องหรือรหัสปลดล็อคเครื่องหรือข้อมูลไบโอเมทริกอื่นๆ ที่ใช้ล็อคอินเข้าเครื่องที่เปิดใช้งานขึ้นมา
    1. เมื่อใส่ข้อมูลที่ร้องขอลงไปแล้วเพื่อยืนยันตัวตนเรียบร้อยจะได้ดังภาพ
    1. คลิก Done เป็นอันเสร็จ
    • จบขอให้สนุก
  • วิธีตั้งค่า MFA (Multi-Factor Authentication) บนบัญชี Google

    วิธีตั้งค่า MFA (Multi-Factor Authentication) บนบัญชี Google

    เร็วๆ นี้บริการของ Google ของมหาวิทยาลัย จะต้องทำการเปิดการยืนยันตัวตนหลายขั้นตอน 2FA (Two-Factor Authentication) หรือ MFA (Multi Factor Authentication) สำหรับผู้ที่ไม่เคยตั้งค่า 2FA สามารถทำได้ดังนี้

    1. ล็อคอินให้เรียบร้อยจะได้ดังภาพ
    1. คลิกที่ Security จะได้ดังภาพ
    1. คลิก + Set up authenticator จะได้ QR code ขึ้นมาดังภาพ
    1. เปิดแอ็พ Microsoft Authenticator ที่เคยใช้งานอยู่กับ Microsoft อยู่แล้ว จะได้ไม่ต้องลงแอ็พอื่น ๆ เพิ่มเติม
    2. เมื่อเปิดแอ็พ Microsoft Authenticator จะได้ดังภาพ
    1. กดเครื่องหมาย + ที่มุมบนขวา จะได้ดังภาพ
    1. เมื่อสแกน QR Code เสร็จ ในมือถือจะได้ดังภาพ
    1. ให้กด Next ในเว็บจะได้หน้าดังภาพ ให้นำตัวเลขจาก Microsoft Authenticator ในส่วนของ Google มาใส่แล้วกด Verify
    1. จะได้หน้าดังภาพ
    1. คลิก Turn on อีกครั้งจะได้หน้าดังภาพ
    1. คลิก Turn on 2-Step Verification แล้วกด Done
    1. จะได้ดังภาพ
    1. กดลูกศร ย้อนกลับมาดูที่หน้า Security ในส่วนของ How you sign in to Google หัวข้อ 2-Step Verification ต้องมีสถานะเป็น On ดังภาพ
    1. ทดสอบล็อคเอาท์และล็อคอินใหม่ เมื่อใส่รหัสผ่านแล้วจะมีหน้า 2-Step Verification ขึ้นมาถาม ให้เปิดแอ็พ Microsoft Authenticator นำรหัสจากแอ็พในหัวข้อ Google มาใส่แล้วกด Next ก็จะเข้าระบบได้เรียบร้อย
    • จบขอให้สนุก
  • Passkey in Microsoft Authenticator

    Passkey in Microsoft Authenticator

    Passkey คือ กลไกการยืนยันตัวตนที่ใช้หลักการของ cryptographic key pair (คู่กุญแจเข้ารหัส) แทนการใช้รหัสผ่าน — chatgpt

    อะไรอีก!!! Passkey ได้ถูกเปิดให้ใช้งานได้แล้วใน Microsoft 365 ของมหาวิทยาลัย (มาแบบไม่ตั้งตัว หลังจากที่รออยู่หลายเดือน) …

    รายละเอียดคร่าว ๆ จาก chatgpt

    ✅ ข้อดีของ Passkey in Microsoft

    หัวข้อรายละเอียด
    🔐 ปลอดภัยกว่า PasswordPasskey ใช้การเข้ารหัสแบบคู่กุญแจ (public-private key) ซึ่งลดความเสี่ยงจาก phishing, credential stuffing และการรั่วไหลของรหัสผ่าน
    👆 ใช้งานง่ายไม่ต้องจำรหัสผ่านอีกต่อไป ใช้เพียง biometric (เช่น สแกนนิ้ว หรือ Face ID) หรือ PIN
    📱 ผูกกับอุปกรณ์Passkey ที่ใช้กับ Microsoft Authenticator จะผูกกับอุปกรณ์และยืนยันตัวตนในเครื่องเท่านั้น
    ☁️ ไม่ต้องพึ่ง SMS/OTPไม่ต้องรอรหัส OTP ทาง SMS หรืออีเมลที่อาจถูกดักจับได้
    🧑‍💼 เหมาะกับองค์กรรองรับการจัดการผ่าน Microsoft Entra ID (Azure AD เดิม), ตั้งค่าบังคับใช้นโยบาย MFA หรือ Passwordless ได้ง่าย
    💼 ผูกกับแอป Authenticator โดยตรงMicrosoft Authenticator ช่วยจัดการ passkey ได้โดยตรงในแอปเดียว

    ❌ ข้อเสียของ Passkey in Microsoft

    หัวข้อรายละเอียด
    🔒 ขึ้นกับอุปกรณ์หากอุปกรณ์หาย หรือถูก factory reset โดยไม่ได้สำรองข้อมูล passkey อาจต้องตั้งค่าใหม่ทั้งหมด
    🔄 ยังไม่ sync ได้ทุกกรณีPasskey ของ Microsoft ยังไม่สามารถ sync ข้ามอุปกรณ์ได้ในบางกรณี (โดยเฉพาะในองค์กรแบบ hybrid หรือที่ยังไม่รองรับ synced passkey เต็มตัว)
    🛠 ต้องตั้งค่าล่วงหน้าต้องมีการเตรียมความพร้อม เช่น เปิดใช้งาน FIDO2 ใน Entra ID, ผูกอุปกรณ์ และมีสิทธิ์ในการจัดการนโยบาย
    🌐 ยังมีเว็บไซต์/ระบบที่ไม่รองรับไม่ใช่ทุกบริการของ Microsoft หรือ third-party ที่รองรับ passkey (โดยเฉพาะระบบ legacy)
    👥 ความเข้าใจของผู้ใช้ผู้ใช้ทั่วไปอาจยังไม่คุ้นเคยกับแนวคิด passkey ทำให้เกิดความสับสนช่วงเริ่มต้นใช้งาน

    ซึ่งถ้าใช้เป็นแล้วชีวิตจะง่ายขึ้นกว่าเดิม (หรือเปล่า) มาถึงวิธีการตั้งค่า

    1. เปิดแอ็พ Microsoft Authenticator ในมือถือ
    1. แตะ 1 ครั้งที่บัญชีของ Prince of Songkhla Unive… จะได้ดังภาพ เลือก Create a passkey
    1. ได้ดังภาพคลิก Sign in
    1. ได้ดังภาพกด Send notification
    1. จะปรากฏตัวเลขให้ยืนยันตัวตนดังภาพ
    1. รอสักครู่จะมีช่องปรากฎให้ใส่ตัวเลข
    1. กรอกตัวเลขลงไป
    1. กด yes เพื่ออนุมัติการลงชื่อเข้าใช้ รอสักครู่กด Done เป็นอันเสร็จ
    1. โดยปกติหากเปิดใช้งาน Passkey แล้ววิธีการจะเปลี่ยนเป็น Passkey โดยปริยาย จะปรากฎคิวอาร์โค้ดขึ้นมาดังภาพ
    1. ให้นำมือถือเครื่องที่ได้สร้าง Passkey เอาไว้ เปิดแอ็พกล้องถ่ายรูปธรรมดา หรืออาจจะใข้ไลน์ (รูปถัดมา) มาแสกนคิวอาร์โค้ดดังกล่าว ให้มีข้อความว่า Sign in with a passkey ปรากฎขึ้นมา เอานิ้วจิ้มที่ข้อความนั้น
    1. เอานิ้วจิ้มที่ข้อควาาม Sign in with a passkey หรือ Tap here to open link จะได้ดังภาพ
    1. เมื่อกด Continue ในมือถือ รอสักครู่ระบบจะเข้าสู่ระบบให้ท้นที
    2. จบขอให้สนุก
  • PSU One Passport (Authentik)

    PSU One Passport (Authentik)

    • >>Onepassport<<คลิกเพื่อเปิดเว็บ onepassport.psu.ac.th
    • PSU One Passport (Authentik) เป็นระบบล็อคอิน แบบใหม่ที่ ระบบเงินเดือน มหาวิทยาลัยสงขลานครินทร์ “ต้องใช้” เพื่อเพิ่มความปลอดภัย ซึ่งระบบล็อคอินนี้ได้ประกาศใช้งานมาเป็นระยะเวลาหนึ่งแล้ว ตั้งแต่ 2 พฤศจิกายน 2567
    • หน้าตาเป็นดังภาพ

    คลิก “ลงชื่อเข้าใช้ด้วย PSU PASSPORT” จะได้หน้าดังภาพ

    เมื่อได้หน้านี้มี 2 ทางเลือกคือ

    1. กรอก Username ของ PSU Passport ลงไปแล้วคลิก Log in
    1. กรอกรหัสผ่านของ PSU Passport ลงไปคลิก Continue
    1. !!สำคัญมาก ให้คลิกเลือกคำว่า “TOTP Device” จะได้ QR Code ดังภาพ
    1. ให้ใช้โปรแกรมจำพวก Authenticator มาสแกน QR Code ดังกล่าวซึ่งโปรแกรมที่เรามีทุกคนอยู่แล้วคือ Microsoft Authenticator นั่นเอง!!!
    1. ในโทรศัพท์จิ้มเครื่องหมาย + (บวก) เลือก Other (Google, Facebook, etc.)
    1. ทำการสแกน QR Code ในข้อ 3. จะได้ PSU One Passport เพิ่มเข้ามาดังภาพ
    1. กลับมาที่ Browser ในคอมพิวเตอร์ให้เลื่อนจอลงมาล่างสุดในหน้าที่มี QR Code จะมีช่องให้ใส่ Code *
    1. ให้นำ Code จาก PSU One Passport ใน App Microsoft Authenticator มาใส่ โดยคลิกที่ คำว่า PSU One Passport จะปรากฎเลข 6 หลัก
    1. นำเลขมาใส่ในช่อง Code * แล้วกด Continue
    1. จะสามารถเข้าระบบที่ใช้งาน Authentik ในการเข้าระบบได้ ในที่นี้คือ https://payroll.urmo.psu.ac.th
    1. จะได้หน้าล็อคอินเข้าระบบอีเมล ของมหาวิทยาลัย

    1. กรอกชื่อบัญชีอีเมลมหาวิทยลัยลงไปแล้วคลิก Next
    1. บางคนจะได้หน้า Approve sign in ที่ปรากฎเลขสองหลัก ให้นำเลขสองหลักไปใส่ใน App Microsoft Authenticator แล้วกดอนุมัติ
    1. หรือบางคนจะได้หน้ากรอกรหัสผ่าน ก็ให้ใส่รหัสผ่านของอีเมลลงไปแล้วจะได้หน้าในข้อที่ 3 เช่นเดียวกัน
    2. นำเลขที่ได้มาใส่ในมือถือ แล้วกด Yes แล้วใส่รหัสปลดล็อคมือถือ (หรือ สแกนนิ้วมือ สแกนหน้า)
    1. จะได้ดังภาพ
    1. ตอบ Yes ได้ถ้าเป็นเครื่องส่วนตัว ถ้าไม่ใช่เครื่องส่วนตัวให้ตอบ No
    2. เข้าระบบได้เรียบร้อย
    3. ถ้าไม่เคยตั้งค่าไมโครซอฟท์ออเทนติเคเตอร์ ให้ทำการตั้งค่าก่อน ตามคู่มือที่ https://sysadmin.psu.ac.th/microsoft-authenticator/
    • ซึ่งแนะนำให้ใช้วิธีที่ 2 ดีกว่า เนื่องจากไม่ต้องตั้งค่าอะไรเพิ่มเติมอีก