Plugin Directory

Changeset 3365740


Ignore:
Timestamp:
09/22/2025 11:26:26 AM (6 months ago)
Author:
codegee0958
Message:

Update: add preview direct-access blocker; bump version to 1.3.0

Location:
ghostgate/trunk
Files:
10 edited

Legend:

Unmodified
Added
Removed
  • ghostgate/trunk/ghostgate.php

    r3363903 r3365740  
    44 * Plugin URI: https://arce-experience.com/product/
    55 * Description: ログインURLを隠して、2FA認証やDoS遮断も可能なWordPress専用セキュリティ強化ツールです。WordPress Login Hardening Plugin.
    6  * Version: 1.2.1
     6 * Version: 1.3.0
    77 * Author: ジー(Code GEE)
    88 * Author URI: https://arce-experience.com/developer/
     
    1818
    1919// 定数定義
    20 define('GHOSTGATE_VERSION', '1.2.1');
     20define('GHOSTGATE_VERSION', '1.3.0');
    2121define('GHOSTGATE_PATH', plugin_dir_path(__FILE__));
    2222define('GHOSTGATE_URL', plugin_dir_url(__FILE__));
  • ghostgate/trunk/inc/admin-actions.php

    r3351716 r3365740  
    55
    66function ghostgate_handle_unblock_ip() {
    7     // メニューと同じ権限に揃える(必要ならフィルタで変更)
    8     $cap = apply_filters( 'ghostgate_required_capability', 'manage_options' );
    9     if ( ! current_user_can( $cap ) ) {
    10         wp_die( esc_html__( 'You do not have permission.', 'ghostgate' ), '', array( 'response' => 403 ) );
    11     }
     7    // メニューと同じ権限(必要ならフィルタで変更)
     8    $cap = apply_filters( 'ghostgate_required_capability', 'manage_options' );
     9    if ( ! current_user_can( $cap ) ) {
     10        wp_die( esc_html__( 'You do not have permission.', 'ghostgate' ), '', array( 'response' => 403 ) );
     11    }
    1212
    13     check_admin_referer( 'ghostgate_unblock_ip_action' );
     13    check_admin_referer( 'ghostgate_unblock_ip_action' );
    1414
    15     // 単体ボタン(name="ip") と一括チェックボックス(name="ip[]") の両対応($_POST 直参照をやめる)
    16     $ips_raw = filter_input( INPUT_POST, 'ip', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
    17     if ( null === $ips_raw ) {
    18         $one = filter_input( INPUT_POST, 'ip', FILTER_UNSAFE_RAW ); // 単体のとき
    19         $ips_raw = is_string( $one ) && $one !== '' ? array( $one ) : array();
    20     }
     15    // --- 入力の安全な取り出し($_POSTを必ず配列化) ---
     16    $ips_raw = array();
     17    if ( isset( $_POST['ip'] ) ) {
     18        $val = $_POST['ip'];
     19        // ボタン(name="ip") or チェックボックス(name="ip[]") の両対応
     20        $ips_raw = is_array( $val ) ? $val : array( $val );
     21    }
    2122
    22     // Unslash → サニタイズ(文字列のみ処理)
    23     $ips_raw = array_map(
    24         static function ( $v ) {
    25             if ( ! is_string( $v ) ) {
    26                 return '';
    27             }
    28             $v = wp_unslash( $v );
    29             return sanitize_text_field( $v );
    30         },
    31         $ips_raw
    32     );
     23    // --- 正規化(unslash → sanitize) ---
     24    $ips_raw = array_map(
     25        static function ( $v ) {
     26            $v = is_string( $v ) ? $v : '';
     27            $v = wp_unslash( $v );
     28            return sanitize_text_field( $v );
     29        },
     30        $ips_raw
     31    );
    3332
    34     // 正規化 & 検証(IPv4/IPv6のみ)
    35     $ips = array_values(
    36         array_filter(
    37             array_unique(
    38                 array_map(
    39                     static function ( $v ) {
    40                         // 念のため許可文字しぼり(sanitization 後だが二重に守る)
    41                         $v = preg_replace( '/[^0-9a-fA-F:\.\-]/', '', (string) $v );
    42                         return filter_var( $v, FILTER_VALIDATE_IP ) ? $v : '';
    43                     },
    44                     $ips_raw
    45                 )
    46             )
    47         )
    48     );
     33    // --- 検証(IPv4/IPv6 のみ許可) ---
     34    $ips = array_values(
     35        array_filter(
     36            array_unique(
     37                array_map(
     38                    static function ( $v ) {
     39                        // 念のため許可文字だけ残す
     40                        $v = preg_replace( '/[^0-9a-fA-F:\.\-]/', '', (string) $v );
     41                        return filter_var( $v, FILTER_VALIDATE_IP ) ? $v : '';
     42                    },
     43                    $ips_raw
     44                )
     45            )
     46        )
     47    );
    4948
    50     foreach ( $ips as $ip ) {
    51         if ( function_exists( 'ghostgate_unblock_ip' ) ) {
    52             ghostgate_unblock_ip( $ip );
    53         }
    54     }
     49    // --- 未選択ならそのまま戻る ---
     50    $dst = wp_get_referer();
     51    if ( ! $dst ) {
     52        $dst = is_network_admin()
     53            ? network_admin_url( 'admin.php?page=ghostgate' )
     54            : admin_url( 'admin.php?page=ghostgate' );
     55    }
    5556
    56     // ★ 元のページ(Referer)に戻るだけ
    57     $dst = wp_get_referer();
    58     if ( ! $dst ) {
    59         // 参照元が取れない場合の保険(トップレベルメニュー想定)
    60         $dst = is_network_admin()
    61             ? network_admin_url( 'admin.php?page=ghostgate' )
    62             : admin_url( 'admin.php?page=ghostgate' );
    63     }
    64     // 同一ホストに限定
    65     $dst = wp_validate_redirect( $dst, admin_url( 'admin.php?page=ghostgate' ) );
     57    if ( empty( $ips ) ) {
     58        $dst = add_query_arg( array( 'ghostgate_msg' => 'no_targets' ), $dst );
     59        wp_safe_redirect( wp_validate_redirect( $dst, admin_url( 'admin.php?page=ghostgate' ) ) );
     60        exit;
     61    }
    6662
    67     wp_safe_redirect( $dst );
    68     exit;
     63    // --- 解除実行 ---
     64    foreach ( $ips as $ip ) {
     65        if ( function_exists( 'ghostgate_unblock_ip' ) ) {
     66            ghostgate_unblock_ip( $ip );
     67        }
     68    }
     69
     70    // --- 戻り先へ(件数付きメッセージ) ---
     71    $dst = add_query_arg(
     72        array(
     73            'ghostgate_msg' => 'unblocked',
     74            'count'         => count( $ips ),
     75        ),
     76        $dst
     77    );
     78    wp_safe_redirect( wp_validate_redirect( $dst, admin_url( 'admin.php?page=ghostgate' ) ) );
     79    exit;
    6980}
    70 
  • ghostgate/trunk/inc/admin-ui.php

    r3363903 r3365740  
    1717    );
    1818});
     19
     20
     21if ( ! function_exists('ghostgate_sanitize_allowed_routes') ) {
     22  function ghostgate_sanitize_allowed_routes( $value ) {
     23    if ( empty( $value ) || ! is_array( $value ) ) return array();
     24
     25    global $ghostgate_bypass_json_filter;
     26    $old_bypass = $ghostgate_bypass_json_filter ?? false;
     27    $ghostgate_bypass_json_filter = true;
     28
     29    if ( ! did_action( 'rest_api_init' ) ) {
     30      do_action( 'rest_api_init' );
     31    }
     32    $server = function_exists('rest_get_server') ? rest_get_server() : null;
     33    $all_routes = $server ? array_keys( $server->get_routes() ) : array();
     34
     35    $ghostgate_bypass_json_filter = $old_bypass;
     36
     37    $value = array_map( 'strval', $value );
     38    $value = array_map( 'wp_check_invalid_utf8', $value );
     39
     40    // 実在ルートとの突き合わせ + 重複除去
     41    $whitelist = array_values( array_unique( array_intersect( $all_routes, $value ) ) );
     42
     43    // `/` は保険で常に含める
     44    if ( ! in_array( '/', $whitelist, true ) ) {
     45      $whitelist[] = '/';
     46    }
     47    return $whitelist;
     48  }
     49}
     50
     51
     52if ( ! function_exists('ghostgate_sanitize_allowed_prefixes') ) {
     53  function ghostgate_sanitize_allowed_prefixes( $value ) {
     54    $value = is_string( $value ) ? wp_unslash( $value ) : '';
     55    $parts = array_map( 'trim', explode( ',', $value ) );
     56
     57    $norms = array();
     58    foreach ( $parts as $p ) {
     59      if ( $p === '' ) continue;
     60
     61      // 許可する文字は「英数/_- とスラッシュ・%・.」くらいまで緩める(REST名前空間の実情に合わせる)
     62      // 余計な空白などは削除
     63      $p = preg_replace( '#[^A-Za-z0-9/_\.\-%]#', '', $p );
     64      if ( $p === '' ) continue;
     65
     66      // 先頭スラッシュ付与、連続スラッシュを1本に
     67      if ( strpos( $p, '/' ) !== 0 ) $p = '/' . $p;
     68      $p = preg_replace( '#/{2,}#', '/', $p );
     69
     70      $norms[] = $p;
     71    }
     72
     73    // 重複除去して、保存/表示はカンマ区切り
     74    $norms = array_values( array_unique( $norms ) );
     75    return implode( ', ', $norms );
     76  }
     77}
     78
     79
    1980
    2081function ghostgate_render_settings_page() {
     
    605666        'ghostgate_rest_limit_cooldown'   => 'absint',
    606667        'ghostgate_hide_json_endpoints' => 'absint',
    607         'ghostgate_json_allowed_routes'   => 'ghostgate_sanitize_array',
    608         'ghostgate_json_allowed_prefixes' => 'sanitize_text_field',
     668        //'ghostgate_json_allowed_routes'   => 'ghostgate_sanitize_array',
     669        //'ghostgate_json_allowed_prefixes' => 'sanitize_text_field',
    609670        'ghostgate_enable_session_control' => 'absint',
    610671        'ghostgate_session_timeout'        => 'absint',
     
    620681        ]);
    621682    }
     683
     684    // 1) 許可ルート(配列)— 正規表現ルートを壊さず、存在ルートとの積集合のみ通す
     685    register_setting(
     686        'ghostgate_options',                   // ← 既存の settings group に合わせる
     687        'ghostgate_json_allowed_routes',
     688        array(
     689            'type'              => 'array',
     690            'sanitize_callback' => 'ghostgate_sanitize_allowed_routes',
     691            'default'           => array(),
     692            'show_in_rest'      => false,
     693        )
     694    );
     695
     696    // 2) 接頭辞(文字列だが正規化は独自)— "/gbrl, /line/v1" のように整える
     697    register_setting(
     698        'ghostgate_options',
     699        'ghostgate_json_allowed_prefixes',
     700        array(
     701            'type'              => 'string',
     702            'sanitize_callback' => 'ghostgate_sanitize_allowed_prefixes',
     703            'default'           => '',
     704            'show_in_rest'      => false,
     705        )
     706    );
     707
     708    // 3) トグル(ON/OFF)
     709    register_setting(
     710        'ghostgate_options',
     711        'ghostgate_hide_json_endpoints',
     712        array(
     713            'type'              => 'string',
     714            'sanitize_callback' => static function( $v ) { return ( $v === '1' ) ? '1' : '0'; },
     715            'default'           => '0',
     716            'show_in_rest'      => false,
     717        )
     718    );
    622719
    623720    // セクション(1つでOK)
     
    34523549        $base_url . 'js/ghost-script-adminui.js',
    34533550        [],
    3454         defined('GHOSTGATE_VERSION') ? GHOSTGATE_VERSION : '1.2.1',
     3551        defined('GHOSTGATE_VERSION') ? GHOSTGATE_VERSION : '1.3.0',
    34553552        true
    34563553    );
     
    34613558        $base_url . 'css/ghost-style-adminui.css',
    34623559        [],
    3463         defined('GHOSTGATE_VERSION') ? GHOSTGATE_VERSION : '1.2.1'
     3560        defined('GHOSTGATE_VERSION') ? GHOSTGATE_VERSION : '1.3.0'
    34643561    );
    34653562}
  • ghostgate/trunk/inc/core.php

    r3351716 r3365740  
    473473
    474474//json秘匿化処理
    475 add_filter('rest_endpoints', 'ghostgate_hide_json_endpoints', 102);
    476 function ghostgate_hide_json_endpoints($endpoints) {
     475add_filter( 'rest_endpoints', 'ghostgate_hide_json_endpoints', 102 );
     476function ghostgate_hide_json_endpoints( $endpoints ) {
     477    // REST サーバ不在なら何もしない(早期リターン)
     478    if ( ! function_exists( 'rest_get_server' ) ) return $endpoints;
     479
    477480    global $ghostgate_bypass_json_filter;
    478481
    479     // 秘匿OFF または UI構築中はスキップ
    480     if (!get_option('ghostgate_hide_json_endpoints') || !empty($ghostgate_bypass_json_filter)) {
     482    // ✅ 秘匿 OFF or UI 構築中(バイパス中)は何もしない
     483    $enabled = get_option( 'ghostgate_hide_json_endpoints', '0' );
     484    if ( $enabled !== '1' || ! empty( $ghostgate_bypass_json_filter ) ) {
    481485        return $endpoints;
    482486    }
    483487
    484     // 許可ルート・接頭辞の取得と整形
    485     $allowed_routes  = get_option('ghostgate_json_allowed_routes', []);
    486     $prefixes_string = get_option('ghostgate_json_allowed_prefixes', '');
    487 
    488     if (!is_array($allowed_routes)) $allowed_routes = [];
    489     $allowed_routes = array_map('strval', $allowed_routes);
    490 
    491     // `/` は必ず許可
    492     if (!in_array('/', $allowed_routes, true)) {
     488    // ✅ 許可ルートの取得(配列のみ許可)— 正規表現を壊さない
     489    $allowed_routes = get_option( 'ghostgate_json_allowed_routes', array() );
     490    if ( ! is_array( $allowed_routes ) ) $allowed_routes = array();
     491    // 文字列化 & UTF-8 妥当性のみ(sanitize_key などは絶対に使わない)
     492    $allowed_routes = array_map( 'strval', $allowed_routes );
     493    $allowed_routes = array_map( 'wp_check_invalid_utf8', $allowed_routes );
     494
     495    // ✅ `/` は常に許可(ルート一覧のルート)
     496    if ( ! in_array( '/', $allowed_routes, true ) ) {
    493497        $allowed_routes[] = '/';
    494498    }
    495499
    496     $prefixes = array_filter(array_map(function($p) {
    497         $p = trim($p);
    498         return ($p === '') ? '' : ((strpos($p, '/') === 0) ? $p : '/' . $p);
    499     }, explode(',', $prefixes_string)));
    500 
    501     // ルートをフィルタ
    502     foreach ($endpoints as $route => $handler) {
    503         if (in_array($route, $allowed_routes, true)) {
     500    // ✅ 自作プレフィックス(カンマ区切り → 正規化)
     501    $prefixes_raw = (string) get_option( 'ghostgate_json_allowed_prefixes', '' );
     502    $prefixes = array_filter( array_map( static function( $p ) {
     503        $p = trim( (string) $p );
     504        if ( $p === '' ) return '';
     505        // 先頭に `/` を付与、連続スラッシュも畳む
     506        $p = ( strpos( $p, '/' ) === 0 ) ? $p : "/{$p}";
     507        $p = preg_replace( '#/{2,}#', '/', $p );
     508        return $p;
     509    }, explode( ',', $prefixes_raw ) ) );
     510
     511    // ✅ 高速化のため、許可ルートを set 化
     512    $allow_set = array_fill_keys( $allowed_routes, true );
     513
     514    // ✅ フィルタリング
     515    foreach ( $endpoints as $route => $handler ) {
     516        // 完全一致で許可
     517        if ( isset( $allow_set[ $route ] ) ) {
    504518            continue;
    505519        }
    506         foreach ($prefixes as $prefix) {
    507             if ($prefix !== '' && strpos($route, $prefix) === 0) {
    508                 continue 2;
    509             }
    510         }
    511         unset($endpoints[$route]);
     520        // 接頭辞で許可
     521        $allowed_by_prefix = false;
     522        foreach ( $prefixes as $px ) {
     523            if ( $px !== '' && strpos( $route, $px ) === 0 ) {
     524                $allowed_by_prefix = true;
     525                break;
     526            }
     527        }
     528        if ( $allowed_by_prefix ) {
     529            continue;
     530        }
     531
     532        // ここまで来たら非表示
     533        unset( $endpoints[ $route ] );
    512534    }
    513535
    514536    return $endpoints;
    515537}
     538
    516539
    517540
     
    786809    }
    787810}
     811
     812
  • ghostgate/trunk/inc/feature-login-slug.php

    r3363903 r3365740  
    321321    $handle = 'ghostgate-disable-remember';
    322322    $src    = GHOSTGATE_URL . 'assets/js/ghost-disable-remember.js';
    323     $ver    = defined( 'GHOSTGATE_VERSION' ) ? GHOSTGATE_VERSION : '1.2.1';
     323    $ver    = defined( 'GHOSTGATE_VERSION' ) ? GHOSTGATE_VERSION : '1.3.0';
    324324
    325325    // セッションが残っていた場合のクリア処理(ログアウト後の安全対策)
  • ghostgate/trunk/inc/session-manager.php

    r3363903 r3365740  
    9494    $handle = 'ghostgate-session-tracker';
    9595    $src    = GHOSTGATE_URL . 'assets/js/ghost-session-tracker.js';
    96     $ver    = defined( 'GHOSTGATE_VERSION' ) ? GHOSTGATE_VERSION : '1.2.1';
     96    $ver    = defined( 'GHOSTGATE_VERSION' ) ? GHOSTGATE_VERSION : '1.3.0';
    9797
    9898    // UIは秒、JSにはmsで渡す。未設定時は5秒。
  • ghostgate/trunk/inc/template-functions.php

    r3363903 r3365740  
    4444    $style_handle  = 'ghostgate-code-style';
    4545    $style_url     = GHOSTGATE_URL . 'assets/css/ghost-style.css';
    46     $version       = defined('GHOSTGATE_VERSION') ? GHOSTGATE_VERSION : '1.2.1';
     46    $version       = defined('GHOSTGATE_VERSION') ? GHOSTGATE_VERSION : '1.3.0';
    4747
    4848    // JS注入
  • ghostgate/trunk/inc/two-factor-auth.php

    r3363903 r3365740  
    143143            plugins_url( '../assets/css/ghost-style-2fa.css', __FILE__ ),
    144144            array(),
    145             defined( 'GHOSTGATE_VERSION' ) ? GHOSTGATE_VERSION : '1.2.1'
     145            defined( 'GHOSTGATE_VERSION' ) ? GHOSTGATE_VERSION : '1.3.0'
    146146        );
    147147    }
  • ghostgate/trunk/languages/ghostgate.pot

    r3363903 r3365740  
    33msgid ""
    44msgstr ""
    5 "Project-Id-Version: GhostGate 1.2.1\n"
     5"Project-Id-Version: GhostGate 1.3.0\n"
    66"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/ghostgate\n"
    77"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
  • ghostgate/trunk/readme.txt

    r3363903 r3365740  
    55Tested up to: 6.8
    66Requires PHP: 7.4
    7 Stable tag: 1.2.1
     7Stable tag: 1.3.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    7070
    7171== Changelog ==
     72= 1.3.0 - 2025-09-22 =
     73* Security: Strengthened “Hide wp-json structure” — allowlist now stores **only actually registered routes** (including regex routes) and never breaks parameterized patterns.
     74* Fix: Route allowlist UI now correctly preserves selections for regex endpoints such as `/gbrl/v1/notify/(?P<slug>[^/]+)` and nested variants.
     75* Fix: Resolved rare fatal error on “Unblock IP” admin action by hardening input handling (supports single `ip` and `ip[]`, sanitizes/validates IPv4/IPv6, safe redirect).
     76* Dev: Added `ghostgate_sanitize_allowed_routes()` and `ghostgate_sanitize_allowed_prefixes()`; introduced a temporary bypass flag so the settings UI can enumerate all routes without being filtered by itself.
     77* Dev: Always whitelists `/` root in `rest_endpoints` filter; normalized custom prefixes (auto-leading slash, condensed duplicate slashes).
     78* Perf: Reduced overhead when building the REST route list on the settings page.
     79* Tweak: Copy and help text polish in settings; minor CSS/UI adjustments.
     80* Tested: Confirmed compatibility with WordPress 6.8.
     81
    7282= 1.2.1 =
    7383* Tweak: Added brand header (logo + subtitle) to the code entry screen with Retina and dark mode support, plus minor a11y improvements.
Note: See TracChangeset for help on using the changeset viewer.