הליבה מציעה שני זוגות פונקציות הצפנה: MISC::encode / MISC::decode, שעליהם נשענים כמעט כל הטוקנים והעוגיות של המערכת, ו-CRYPT::encode / CRYPT::decode, ספרייה נקייה יותר שהליבה עצמה לא משתמשת בה. בעמוד הזה מתוארים שניהם, ההבדלים ביניהם, ומה אתם חייבים להגדיר כדי שהמפתחות לא יהיו ברירת המחדל הציבורית.
system/ בעמוד הזה הוא תיקיית הליבה הפרוסה (api/core בריפו), ו-{admin} הוא CONFIG::$admin_url.
MISC::encode מגיע עם מפתח ו-IV קבועים בקוד ('This is my secret key' ו-'This is my secret iv'), ו-CRYPT עם מפתח ברירת מחדל 'wizzocms'. הליבה לא מגדירה מפתחות משלה: אתר שלא קורא ל-MISC::set_encrypt_secret_keys() ב-application/includes/init.php רץ עם מפתחות שכל מי שקרא את הקוד מכיר. מי שמכיר את המפתח יכול לפענח וגם לזייף כל טוקן, עוגייה ושדה מוצפן שהאתר מנפיק. זה ממצאים SEC-02 ו-CORE-01 בביקורת, והסעיף הראשון בצ'קליסט ההתקשחות.MISC::encode ו-MISC::decode#
public static function encode($str)
public static function decode($str)
encode מקבל כל ערך ש-PHP יכול לבצע לו serialize (מחרוזת, מספר, מערך) ומחזיר מחרוזת בטוחה לשימוש ב-URL ובעוגייה. decode מבצע את הפעולה ההפוכה ומחזיר את הערך המקורי.
| פונקציה | פרמטר | מחזירה |
|---|---|---|
MISC::encode($str) | כל ערך שניתן לסריאליזציה | מחרוזת מקודדת; מחרוזת ריקה מחזירה "" |
MISC::decode($str) | מחרוזת שנוצרה ב-encode | הערך המקורי, או false בפענוח כושל |
$token = MISC::encode(["uid" => 17, "exp" => time() + 3600]);
// ... מעבירים ב-URL, בעוגייה או בשדה hidden ...
$data = MISC::decode($token);
if (is_array($data) && $data["exp"] > time())
{
// תקף
}
איך זה עובד בפועל#
- הערך עובר
serialize. MISC::encrypt_decrypt('encrypt', ...)מצפין ב-AES-256-CBC. המפתח הואhash('sha256', $encrypt_secret_key), וה-IV הוא 16 התווים הראשונים שלhash('sha256', $encrypt_secret_iv). כלומר ה-IV קבוע, לא אקראי.- התוצאה עוברת
base64_encode(הפונקציה הפנימית כבר מחזירה base64, כך שהתוצאה מקודדת פעמיים), ואזstrtrשמחליף+/=ב--_,כדי שהמחרוזת תהיה בטוחה ל-URL.
המשמעות המעשית של העיצוב הזה:
- אין חתימה (MAC). מי שמכיר את המפתח מייצר טוקן חוקי, ומי שלא מכיר אותו אינו מוגן מפני שינוי בתוכן המוצפן, אלא רק מפני שהפענוח ייתן זבל.
- IV קבוע אומר שאותו ערך תמיד מקודד לאותה מחרוזת. אפשר לזהות ערכים זהים, וזה לא מתאים להצפנת מידע רגיש.
decodeמשתמש ב-unserializeבלי הגבלת מחלקות. כל עוד המפתח סודי זה לא ניתן לניצול מבחוץ; עם מפתח ברירת מחדל, מי שמזייף טוקן יכול להזריק אובייקטים. זה הטיעון החזק ביותר להחליף מפתחות (ממצאים SEC-03 ו-SEC-04).
מי משתמש ב-encode#
יש כ-80 קריאות ל-MISC::encode / decode בליבה. בין היתר:
- עוגיית ההתחברות של משתמשי האתר (
LOGIN,COOKIES::user_log), ראו משתמשי אתר. - שדות מוצפנים בטפסי הניהול, טוקנים של הגלריה ושל כלי החיתוך, וקישורים במיילים.
- הרשימה המותרת של כתובות IP בפאנל הניהול (
allow_ip). - שמות הקבצים של תמונות מטושטשות (
STORAGE::get_blur_image). - עוגיית
admin_tokenהישנה של פאנל הניהול (נתיב תאימות לאחור בלבד).
קביעת מפתחות לאתר#
public static function set_encrypt_secret_keys($key, $iv)
| פרמטר | משמעות |
|---|---|
$key | הסוד שממנו נגזר מפתח ההצפנה |
$iv | הסוד שממנו נגזר ה-IV |
הליבה לא קוראת לפונקציה הזו בשום מקום, ולכן היא אחריות האתר. המקום הנכון הוא application/includes/init.php, שנטען בכל בקשה (כולל CLI ו-cron) אחרי CONFIG::init() ולפני כל קוד שמפענח משהו:
<?php
// application/includes/init.php
// הערכים מגיעים מקובץ מחוץ ל-docroot או מהכספת, לא מוטמעים בקוד שנכנס ל-git.
$keys = require CONFIG::$base_path . "/../secrets/wizzo_keys.php";
MISC::set_encrypt_secret_keys($keys["key"], $keys["iv"]);
CRYPT::set_secret_key($keys["crypt"]);
כל אחד מהערכים צריך להיות מחרוזת אקראית ארוכה, ייחודית לאתר:
openssl rand -hex 32
סיבוב מפתח משנה את הפלט של encode ומבטל כל מה שהוצפן בישן: עוגיות התחברות של משתמשים, קישורים שנשלחו במיילים, טוקנים שמורים בבסיס הנתונים, וגם שמות קבצי התמונות המטושטשות. לכן מגדירים את המפתחות פעם אחת לפני שיש נתונים. אתר קיים שרץ עם ברירת המחדל שאי אפשר להשאיר: מחליפים את המפתח בחלון תחזוקה, ומצפים שכל המשתמשים יתחברו מחדש.
CRYPT#
CRYPT (system/collections/CRYPT.php) היא מחלקה סטטית קטנה שנכתבה נכון יותר: IV אקראי בכל הצפנה, ו-JSON במקום serialize.
CRYPT::set_secret_key($key) // ברירת מחדל: 'wizzocms'
CRYPT::encode($data)
CRYPT::decode($data)
| פונקציה | התנהגות |
|---|---|
set_secret_key($key) | קובעת את המפתח. הוא עובר hash('sha256', $key, true) |
encode($data) | מצפינה ב-AES-256-CBC עם IV אקראי של 16 בתים שמוצמד לתחילת התוצאה. המטען הוא JSON (JSON_UNESCAPED_UNICODE | JSON_NUMERIC_CHECK). הפלט הוא base64 בטוח ל-URL, ללא padding. ערך ריק או null מחזיר "" |
decode($data) | מפענחת ומחזירה את ערך ה-JSON; כשהתוכן המפוענח אינו JSON מוחזרת המחרוזת הגולמית |
$blob = CRYPT::encode(["order" => 4521, "email" => "a@example.co.il"]);
$back = CRYPT::decode($blob); // ["order" => 4521, "email" => "a@example.co.il"]
גם CRYPT לא מחתימה את המטען. מי שצריך לוודא שהתוכן לא שונה (למשל קישור "אשר הזמנה") מוסיף חתימת HMAC משלו:
$payload = CRYPT::encode(["order" => 4521]);
$sig = hash_hmac("sha256", $payload, $keys["hmac"]);
$link = "/confirm?d=" . $payload . "&s=" . $sig;
// בצד המקבל
$ok = hash_equals(hash_hmac("sha256", $_GET["d"], $keys["hmac"]), $_GET["s"]);
MISC או CRYPT#
MISC::encode | CRYPT::encode | |
|---|---|---|
| IV | קבוע (נגזר מהמפתח) | אקראי לכל הצפנה |
| פורמט המטען | serialize | JSON |
| פענוח | unserialize ללא הגבלה | json_decode |
| ברירת מחדל של מפתח | 'This is my secret key' | 'wizzocms' |
| שימוש בליבה | רחב, אי אפשר להימנע | אין |
| מתאים לקוד חדש | לא, אלא כשחייבים תאימות לליבה | כן |
לקוד חדש של האתר בחרו CRYPT. השתמשו ב-MISC::encode רק כשאתם צריכים להבין או לייצר ערכים שהליבה עצמה קוראת, למשל עוגיית התחברות.
system/libraries/encoder.php (המחלקה encoder) היא גרסה ישנה: AES-128-CBC עם מפתח קבוע בקוד ו-gzcompress(serialize()). הליבה לא משתמשת בה ואין סיבה להתחיל להשתמש בה.סיסמאות הן לא עניין של הפונקציות האלה#
אסור להצפין סיסמאות עם encode או CRYPT: סיסמאות עוברות hash חד-כיווני. הליבה כוללת את המחלקה Bcrypt (system/libraries/bcrypt.php) עם hash($input) ו-verify($input, $hash), והפונקציה הגלובלית dt_encode($password) מחזירה hash של bcrypt. פרטים בעמוד משתמשי אתר.