Checklist מינימלי לאבטחת אפליקציות Windows
· עודכן בתאריך: · Go Komura · פיתוח Windows, אבטחה, architecture, C# / .NET, Win32
היסטוריית עדכונים (גרסה ראשונה, פורסמה בתאריך 14 Mar 2026)
- פרסום ראשון
לצטט את המאמר הזה(DOI: 10.5281/zenodo.22173448)
מאמר זה מאוחסן בארכיון Zenodo. להלן גם ה-DOI שתמיד מפנה לגרסה האחרונה וגם ה-DOI המקובע לגרסה שאתם קוראים.
Go Komura (2026). Checklist מינימלי לאבטחת אפליקציות Windows. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22173448 https://comcomponent.com/he/blog/windows-app-security-minimum-checklist/
- DOI (הגרסה האחרונה)
- 10.5281/zenodo.22173448
- DOI (הגרסה הזו)
- 10.5281/zenodo.22173449
הורידו את ה-checklist בגרסת Excel
תוכן הקובץ הזה זהה ל-checklist שלפני release בפרק 4 (8 קטגוריות, 32 סעיפים). ההבדל היחיד הוא שיש בו שדות מילוי Status ו-Notes, ושני גליונות Checklist-ja / Checklist-en שמכילים גם יפנית וגם אנגלית. אם קוראים תוך כדי בדיקה — פרק 4 מספיק. אם מחלקים כרשומת review — גרסת ה-Excel.
כשמדברים על אבטחת אפליקציות Windows, הנושא נוטה להתנפח מהר. Zero Trust, EDR, SBOM (Software Bill of Materials), ניהול certificates, ניהול פגיעויות. כל אלה חשובים, אבל בעבודה מעשית יש לפני כן כמה יסודות שלא כדאי לפספס.
בפרט, באפליקציות מהסוג הבא, סגירת הפרצות הבסיסיות עוזרת יותר מ”הגנה מתקדמת”.
- אפליקציות desktop ב-WPF / WinForms / WinUI
- אפליקציות Win32 ב-C++ / C#
- שילוב עם מכשירים, שילוב קבצים, חיבור ל-DB, כלי הפצה פנימיים בארגון
- אפליקציות עסקיות עם מנגנון update אוטומטי
- הרכב שכולל Windows Services או קבצי EXE נלווים
בפיתוח אפליקציות Windows, מעשי יותר להתחיל מסגירת פרצות שברור שהן מסוכנות, ולא לנסות להשלים הכול בבת אחת.
כאן נעבור לפי סדר תכנון, מימוש, הפצה ותפעול על הנקודות שברור שלא כדאי לפספס.
flowchart TB
accTitle: אופן ההתקדמות של המאמר
accDescr: תרשים שמראה שקודם סוגרים פרצות בסיסיות ורק אז עוברים להגנה מתקדמת, ושהמאמר עובר על הנקודות המינימליות לפי תכנון, מימוש, הפצה ותפעול.
adv1["הגנה מתקדמת (Zero Trust וכדומה)"] -.->|"קודם לכן"| base1["סגירת הפרצות הבסיסיות"]
base1 --> o1["תכנון"]
o1 --> o2["מימוש"]
o2 --> o3["הפצה"]
o3 --> o4["תפעול"]
איור 1: לפני הגנה מתקדמת סוגרים את הפרצות הבסיסיות, ואחר כך עוברים לפי הסדר על תכנון, מימוש, הפצה ותפעול.
1. קודם המסקנה
- הדבר הראשון שלא כדאי לפספס: לא לדרוש הרשאות Administrator שלא לצורך, לחתום, לא להחזיק secrets ב-plaintext, ולא לבטל אימות certificates.
- באפליקציות Windows, קבצי ההפצה עצמם הם משטח תקיפה. בטוח יותר להתייחס גם ל-EXE / DLL / MSI / MSIX / מודול update אוטומטי.
ServerCertificateValidationCallback => true, connection string ב-plaintext, טעינה רשלנית עםLoadLibrary("foo.dll"), והרצת SQL בשרשור מחרוזות — כל אלה מוטב להימנע מהם גם ברף המינימלי.- אם רק חלק מהעבודה דורש הרשאות Administrator, בטוח יותר להפריד רק את אותו חלק ל-process נפרד או ל-Windows Service, ולא להעלות את הרשאת כל האפליקציה.
- כדאי להניח מראש שאפליקציה שמופצת ב-Windows תזדקק לחתימה + timestamp. זה נותן לא רק אמינות בעיני המשתמשים, אלא גם זיהוי שיבוש והסבר תפעולי נוח יותר.
- secrets שנשמרים מקומית משתמשים לפי הצורך ב-DPAPI / ProtectedData או ב-Credential Locker. לפחות כדאי לצאת ממצב שבו הם ב-
appsettings.jsonב-plaintext. - לא כל כמות לוגים טובה. השארת tokens, סיסמאות, connection strings, מידע אישי וגוף request מלא בלוג כמו שהוא הופכת את הלוג עצמו לגורם התקרית.
אבטחה מינימלית אינה הוספת features מיוחדים אלא הימנעות מהשארת התנהגות default מסוכנת או מימוש רשלני.
flowchart TB
accTitle: התפיסה של אבטחה מינימלית
accDescr: תרשים שמראה שאבטחה מינימלית אינה הוספת features מיוחדים אלא הימנעות מהשארת התנהגות default מסוכנת או מימוש רשלני.
add1["הוספת feature מיוחד"] -.->|"זה לא המוקד המינימלי"| goal1["אבטחה מינימלית"]
rm1["לא להשאיר default מסוכן או מימוש רשלני"] -->|"זה המוקד"| goal1
איור 2: הרף המינימלי אינו הוספת features, אלא הימנעות מהשארת default מסוכן או מימוש רשלני.
ב-diagram, solid line מציינת relation שתמיד מתקיים ו-dashed line מציינת relation מותנה (התנאים מופיעים בהסבר של כל relation ב-detail page). הרשימה המלאה של ה-relations (סה”כ 21, כולל evidence ו-certainty) וההגדרות של ה-concepts המרכזיים נמצאות ב-detail page של ה-knowledge map (ביפנית). Data: JSON-LD / Turtle
2. היקף המאמר ומשמעות “מינימום”
2.1. ההיקף שהמאמר מטפל בו
המאמר מתייחס לאפליקציות Windows מהסוג הבא:
- אפליקציות desktop ב-WPF / WinForms / WinUI
- אפליקציות Win32 ב-C++ / C#
- כלי הפצה פנימיים בארגון, כלי שילוב מכשירים, כלי ניטור
- הרכב שכולל EXE נלווה, Windows Service ו-updater
- תוכנה עסקית שמופצת ב-EXE / MSI / MSIX
“המינימום” כאן אינו הצורה הסופית שעוברת audit, אלא הסעיפים שהיעדרם גורם לתקלה בשגרה.
flowchart TB
accTitle: משמעות "המינימום" במאמר הזה
accDescr: תרשים שמראה שהמינימום במאמר הזה אינו הצורה הסופית שעוברת audit, אלא הסעיפים שהיעדרם גורם לתקלה בשגרה.
au1["הצורה הסופית שעוברת audit"] -.->|"לא זו הכוונה כאן"| mn1["המינימום של המאמר הזה"]
ac1["סעיפים שהיעדרם גורם לתקלה בשגרה"] -->|"זו הכוונה"| mn1
איור 3: “המינימום” כאן אינו הצורה הסופית שעוברת audit, אלא הסעיפים שהיעדרם גורם לתקלה בשגרה.
גם הנחות היסוד לדוגמאות הקוד מוגדרות מראש. דוגמאות C# מניחות .NET 8 ואילך, ודוגמאות C++ מניחות Win32 API. גם ב-.NET Framework 4.8 התפיסה נשארת זהה, אך יש מקומות שבהם הכתיבה המומלצת השתנתה. הדוגמה הבולטת היא סביבת ServicePointManager בסעיף 3.6, שבקוד חדש עבר לתפיסה שמשתמשת ב-IHttpClientFactory וב-HttpClient. כשקוראים מחדש קוד מדור ישן, שימו לב במיוחד לנקודה הזו.
2.2. מה לא נכלל
מצד שני, יש דברים שנשארים מחוץ למוקד המאמר.
- תכנון Zero Trust כלל-ארגוני
- תפעול כולל של EDR / SIEM / DLP / MDM
- hardening מפורט של kernel drivers
- תכנון הצפנה מאפס
- ניתוח איומים מתקדם או הליכי forensics
כלומר, לא “מדיניות אבטחה עצומה של הארגון כולו”, אלא קו יסוד שקשה למפתחי אפליקציות Windows להשמיט בכוחות עצמם לפני release.
flowchart TB
accTitle: קו הגבול בין הנכלל למה שלא נכלל
accDescr: תרשים שמראה שמדיניות אבטחה עצומה כלל-ארגונית אינה נכללת, ושהמאמר מתמקד בקו יסוד שקשה למפתחים להשמיט בכוחות עצמם לפני release.
org1["מדיניות עצומה כלל-ארגונית"] -.->|"לא נכלל במאמר הזה"| sc1["ההיקף של המאמר הזה"]
dev1["קו יסוד שקשה למפתח להשמיט בכוחות עצמו"] -->|"זה נכלל"| sc1
איור 4: המאמר לא עוסק במדיניות כלל-ארגונית, אלא בקו היסוד שהמפתח יכול לתפוס בעצמו לפני release.
3. קודם ה-checklist
לפני דיון מפורט, נציג טבלה שמאפשרת מבט כולל. כבר ממנה אפשר לזהות באילו מקומות כדאי לבדוק שוב.
3.1. התמונה הכוללת
| מה בודקים | מה עושים כמינימום | תקלה טיפוסית |
|---|---|---|
| הרשאת הרצה | asInvoker כברירת מחדל, ורק עבודה שדורשת elevation מופרדת |
הפיכת כל האפליקציה ל-requireAdministrator |
| אמינות ההפצה | code signing ל-EXE / DLL / MSI / MSIX, כולל timestamp | הפצה במצב לא חתום |
| update | קיבוע מקור ה-update, וזיהוי שיבוש דרך HTTPS ואימות חתימה | דריסה ישירה אחרי הורדה ב-HTTP |
| secrets | לא להחזיק secrets בקוד המקור או ב-Configuration ב-plaintext, שימוש ב-DPAPI / Credential Locker וכדומה | הצבת API keys או connection strings בקובץ Configuration ב-plaintext |
| תקשורת | שימוש ב-HTTPS ולא ביטול אימות certificates | דילוג קבוע על אימות certificates עם return true |
| קלט חיצוני | validation מלא של SQL, קבצים, IPC, URI, CSV, JSON וכדומה | מעבר חופשי בטענה “זה כלי פנימי בארגון” |
| טעינת DLL | נתיב מוחלט, SetDefaultDllDirectories, סדר חיפוש בטוח |
הסתמכות על הספרייה הנוכחית עם LoadLibrary("foo.dll") |
| לוגים | מיסוך tokens, סיסמאות, PII, והפרדה בין שגיאה למשתמש לשגיאה פנימית | הצגה או שמירה ישירה של פרטי exception או connection string |
| תלויות | עדכון שוטף של SDK, NuGet, VC++ runtime, תלויות OSS | קיבוע לשנים ללא מעקב אחר מידע על פגיעויות |
3.2. הרשאה כברירת מחדל היא asInvoker
זו הנקודה הראשונה שכדאי לבחון מחדש באפליקציית Windows. הרצת כל האפליקציה בהרשאות Administrator גורמת לכך שבאגים, החלפת DLL, קריאה שגויה של קבצי Configuration ופגמים בקלט חיצוני רצים ישירות בהרשאה חזקה.
flowchart TB
accTitle: הסכנה בהרצת כל האפליקציה בהרשאות Administrator
accDescr: תרשים שמראה שהרצת כל האפליקציה בהרשאות Administrator גורמת לכך שבאגים, החלפת DLL ופגמים בקלט חיצוני רצים ישירות בהרשאה חזקה.
all1["הרצת כל האפליקציה בהרשאות Administrator"] --> bug1["באגים"]
all1 --> swp1["החלפת DLL"]
all1 --> inp1["קריאה שגויה של Configuration / פגם בקלט"]
bug1 --> pw1["רץ ישירות בהרשאה חזקה"]
swp1 --> pw1
inp1 --> pw1
איור 5: כשמעלים הרשאה של כל האפליקציה, כל פגם שהוא רץ בהרשאה חזקה.
המדיניות הבסיסית היא כזו:
- אפליקציית UI רגילה רצה ב-
asInvoker - רק עבודה שדורשת הרשאות Administrator מופרדת ל-process נפרד או ל-Windows Service
- elevation מתבצע רק ברגע הנדרש
- גם הקלט שמועבר ל-EXE הנלווה או ל-service מאומת
אם מדובר באפליקציית desktop שרגילה רק לצפייה ולעריכה, ורק ההתקנה או שינוי הגדרות firewall דורשים הרשאות Administrator, בטוח יותר לרכז את מה שדורש elevation ב-broker, ולא להפוך את כל האפליקציה ל-requireAdministrator.
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
“נוח כשהכול רץ כ-Administrator” בדרך כלל חוזר לפגוע בהמשך. הרצה בהרשאה מינימלית, עם הוצאה החוצה רק של הפעולות שבאמת דורשות זאת, מצמצמת משמעותית את רדיוס הפגיעה.
flowchart TB
accTitle: מבנה של הפרדת עבודה שדורשת elevation
accDescr: תרשים שמראה שאפליקציית UI רגילה רצה ב-asInvoker, שרק עבודה שדורשת הרשאות Administrator מרוכזת ב-broker ב-process נפרד או ב-service, ושה-elevation מתבצע רק ברגע הנדרש.
ui1["אפליקציית UI (asInvoker)"] -->|"פנייה רק ברגע הנדרש"| br1["broker (EXE נפרד / service)"]
br1 --> el1["ביצוע רק של פעולות שדורשות elevation"]
ui1 -.-> vd1["גם הקלט ל-broker מאומת"]
איור 6: בדרך כלל מריצים ב-asInvoker, ורק פעולות שדורשות elevation מרוכזות ב-broker, כך שרדיוס הפגיעה מצטמצם.
3.3. חתימה על הבינארי ועל ההתקנה
ב-Windows, אמינות קבצי ההפצה קובעת הכול. המשתמש נוגע לא בקוד המקור, אלא ב-EXE, DLL, MSI, MSIX וב-updater. אם אלה לא חתומים, גם זיהוי השיבוש, גם האמינות בעיני המשתמש וגם ההסבר התפעולי נחלשים.
flowchart TB
accTitle: מה שהמשתמש נוגע בו הם קבצי ההפצה
accDescr: תרשים שמראה שהמשתמש נוגע לא בקוד המקור אלא ב-EXE, DLL, installer ו-updater, ושאם אלה לא חתומים זיהוי השיבוש וההסבר התפעולי נחלשים.
us1["מה שהמשתמש נוגע בו"] --> bin1["EXE / DLL"]
us1 --> pkg1["MSI / MSIX"]
us1 --> upd1["updater"]
bin1 --> ns1["בלי חתימה — זיהוי שיבוש והסבר חלשים"]
pkg1 --> ns1
upd1 --> ns1
איור 7: משטח התקיפה הוא קבצי ההפצה עצמם, ובלי חתימה מאבדים את הבסיס לאמינות.
לכל הפחות, כדאי לבדוק את אלה:
- לחתום על EXE / DLL / MSI / MSIX
- לחתום לא רק על ההתקנה אלא גם על בינאריים נלווים שמשמשים ל-update
- להוסיף timestamp
- לכלול בתהליך ה-release את פקיעת תוקף ה-certificate ואת הליך החידוש
חתימה שאין לה timestamp נוטה לגרום לקושי באימות אחרי פקיעת תוקף ה-certificate. במקום “חתמנו וזהו”, יציב יותר לכלול בהליך ה-release חתימה + timestamp יחד.
flowchart TB
accTitle: חתימה ו-timestamp
accDescr: תרשים שמראה שחתימה בלי timestamp גורמת לקושי באימות אחרי פקיעת תוקף ה-certificate, ושחתימה יחד עם timestamp שנכללת בהליך ה-release יציבה יותר.
sg2["חתימה בלבד"] --> tr1["קושי באימות אחרי פקיעת תוקף ה-certificate"]
ts1["חתימה + timestamp"] --> st2["פחות קושי באימות אחרי הפקיעה"]
ts1 -.-> fl1["נכלל בהליך ה-release"]
איור 8: לא מסתפקים בחתימה בלבד — כוללים גם timestamp בהליך ה-release.
אם משתמשים ב-MSIX, חתימת החבילה היא הנחת יסוד. גם בהפצת MSI / EXE, כדאי לפחות לחתום על גוף ה-installer ועל הבינאריים המרכזיים שרצים.
3.4. קיבוע מסלול ה-update והוספת זיהוי שיבוש
באפליקציית Windows של היום, מסלול ה-update נמצא לרוב בשימוש ממושך יותר מההתקנה הראשונית. אם הוא נעשה ברשלנות, גם אם גוף האפליקציה נבנה בקפידה, ה-updater נהיה החוליה החלשה ביותר.
flowchart TB
accTitle: מסלול ה-update נמצא בשימוש הארוך ביותר
accDescr: תרשים שמראה שמסלול ה-update נמצא בשימוש ממושך יותר מההתקנה הראשונית, ושאם הוא נעשה ברשלנות ה-updater נהיה החוליה החלשה ביותר.
ins1["ההתקנה הראשונית"] -.->|"בשימוש רק פעם אחת"| ap1["lifetime של האפליקציה"]
up2["מסלול ה-update"] -->|"בשימוש ממושך לאורך זמן"| ap1
up2 --> wk2["ברשלנות הוא נהיה החוליה החלשה"]
איור 9: מסלול ה-update נמצא בשימוש ארוך יותר מההתקנה הראשונית, וברשלנות הוא נהיה החוליה החלשה.
סביב ה-update, כדאי לחשוב מראש לפחות על חמישה דברים:
- קבלת קובץ ה-update תמיד דרך HTTPS
- אימות חתימה או hash של קובץ ה-update שהתקבל
- מניעת החלפה חופשית של כתובת ה-URL של מקור ה-update בקוד או ב-Configuration
- חתימה גם על מודול ה-update עצמו
- קביעת נוהל rollback או שחזור במקרה כשלון
אם אפשר לבחור ב-MSIX + App Installer, קל יותר לרכז את מנגנון ה-update קרוב יותר ל-OS. מצד שני, אם יש updater עצמאי, יש לוודא גם את בטיחות התקשורת וגם את האמיתות של קובץ ההפצה. HTTPS לבדו שומר על “ערוץ התקשורת”, אבל לא מבטיח “האם הקובץ הזה באמת פרסום שלנו”.
flowchart TB
accTitle: שני הדברים שמאמתים ב-update
accDescr: תרשים שמראה שב-updater עצמאי צריך לוודא גם את בטיחות התקשורת דרך HTTPS וגם את האמיתות של קובץ ה-update דרך חתימה או hash.
dl1["קבלת קובץ ה-update ב-HTTPS"] --> vf1["אימות חתימה או hash"]
vf1 --> ap2["החלה רק אחרי מעבר האימות"]
dl1 -.-> lim1["HTTPS שומר רק על ערוץ התקשורת"]
vf1 -.-> own1["בדיקה האם זה פרסום שלנו"]
איור 10: גם אם מקבלים ב-HTTPS, האמיתות היא שאלה נפרדת — מיישמים רק אחרי מעבר אימות חתימה או hash.
3.5. לא להחזיק secrets בקוד המקור או ב-Configuration ב-plaintext
זו נקודה שקל מאוד להיכשל בה בעבודה מעשית. בטענה “זה כלי פנימי בארגון” או “בכל מקרה רק מפיצים exe”, נוטים להשאיר connection string, API key, credentials לתיקייה משותפת ו-token קבוע בקוד המקור או בקובץ Configuration.
לכל הפחות, כדאי להימנע מהצבות כאלה:
- API key שכתוב ישירות בקוד המקור
- סיסמה ב-plaintext ב-
appsettings.jsonאו ב-app.config - connection string שנכנסה ל-repository
- תכנון שבו מפתח הפענוח וה-ciphertext נמצאים באותו מקום
- credentials קבועים ומשותפים לכל המשתמשים, במקום per-user
באופן מעשי, באפליקציית Windows יש בעיקר ארבע אפשרויות:
- רוצים לשמור Windows credentials באפליקציית packaged desktop / משפחת WinUI, לשקול Credential Locker
- רוצים לשמור secret מוצפן מקומית
ב-Win32 / .NET, להשתמש ב-DPAPI /
ProtectedData - היעד תומך ב-Windows authentication או integrated authentication אם אפשר, לא להחזיק סיסמה באפליקציה כלל
- ניהול ה-secret אפשרי בענן או בצד השרת להעדיף תכנון שלא מטמיע secret ארוך-טווח בצד הלקוח
ב-C#, לפחות שימוש ב-DPAPI כמו להלן כבר עדיף בהרבה משמירה ב-plaintext. אם כותבים גם את השמירה וגם את הקריאה בהלוך-חזור, זה נראה כך:
// C# / .NET 8. ProtectedData מיועד ל-Windows בלבד, וב-.NET
// דרוש חבילת NuGet בשם System.Security.Cryptography.ProtectedData.
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
public static class SecretStore
{
// גם בפענוח נדרש אותו ערך. אפשר גם null, אך יותר בטוח להוסיף ערך.
private static readonly byte[] Entropy = [0x4b, 0x53, 0x2d, 0x76, 0x31];
public static void Save(string path, string secretText)
{
byte[] plaintext = Encoding.UTF8.GetBytes(secretText);
byte[] ciphertext = ProtectedData.Protect(
plaintext,
Entropy,
DataProtectionScope.CurrentUser);
// אפשר גם לשמור את מערך הבייטים ישירות לקובץ, אך אם מכניסים לקובץ תצורה, עדיף Base64.
File.WriteAllText(path, Convert.ToBase64String(ciphertext));
CryptographicOperations.ZeroMemory(plaintext);
}
public static string Load(string path)
{
byte[] ciphertext = Convert.FromBase64String(File.ReadAllText(path));
// אם לא אותו משתמש ואותו entropy כמו בזמן השמירה, יתקבל CryptographicException.
byte[] plaintext = ProtectedData.Unprotect(
ciphertext,
Entropy,
DataProtectionScope.CurrentUser);
try
{
return Encoding.UTF8.GetString(plaintext);
}
finally
{
CryptographicOperations.ZeroMemory(plaintext);
}
}
}
צד הקריאה נראה כך:
string path = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"SampleApp",
"token.dat");
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
SecretStore.Save(path, "example-api-key");
string restored = SecretStore.Load(path);
מה שחשוב כאן הוא לא “הצפנו אז זה בטוח”, אלא לקבוע בתכנון מי יכול לפענח.
יש הבדל משמעותי בין CurrentUser ל-LocalMachine. CurrentUser מאפשר פענוח רק למשתמש שביצע את השמירה, ו-LocalMachine מאפשר לכל מי שנמצא באותה מכונה. אם ה-service רץ תחת חשבון נפרד, או שיש תפעול שמחליף בין משתמשים, חשוב לקבוע את זה מראש — אחרת ייתקעו אחר כך במצב של “לא ניתן לקרוא” או “ניתן לקרוא יותר מדי”.
flowchart TB
accTitle: מי יכול לפענח ב-DPAPI
accDescr: תרשים שמראה שהגדרת ה-scope של DPAPI ל-CurrentUser מאפשרת פענוח רק למשתמש השומר, ואילו LocalMachine מאפשר פענוח לכל מי שבמכונה, ולכן צריך לקבוע זאת מראש בתכנון.
dc1["קביעה בתכנון: מי יכול לפענח"] -->|"CurrentUser"| cu1["רק המשתמש השומר יכול לפענח"]
dc1 -->|"LocalMachine"| lm1["כל מי שבמכונה יכול לפענח"]
dc1 -.-> lt1["בלי קביעה מראש — תקיעה מאוחר יותר"]
איור 11: ב-DPAPI, בחירת ה-scope קובעת מי יכול לפענח, ולכן קובעים זאת מראש.
חשוב גם לדעת שהמפתח של DPAPI נמצא בפרופיל המשתמש, ולכן כשהפרופיל לא טעון (כגון בזמן impersonation), הפענוח עלול להיכשל — כך כתוב במפורש בתיעוד. אם מתכננים שימוש מ-Windows Service, כדאי לבדוק גם את הנקודה הזו.
flowchart TB
accTitle: המפתח של DPAPI ופרופיל המשתמש
accDescr: תרשים שמראה שהמפתח של DPAPI נמצא בפרופיל המשתמש, ולכן במצבים כמו impersonation שבהם הפרופיל לא טעון, הפענוח נכשל.
ky1["המפתח של DPAPI"] --> pf1["נמצא בפרופיל המשתמש"]
pf1 -->|"פרופיל לא טעון (כגון impersonation)"| fe1["הפענוח נכשל"]
fe1 -.-> sv2["צריך לבדוק זאת בשימוש מ-service"]
איור 12: מפתח ה-DPAPI נמצא בפרופיל המשתמש, וכשהפרופיל לא טעון לא ניתן לפענח.
בחיבור ל-SQL Server, בסביבה מקומית לפעמים אפשר להעדיף Windows authentication כברירה ראשונה.
אם בכל זאת חייבים לכלול credentials ב-connection string, לפחות כדאי לשמור על Persist Security Info=False ולא להשאיר אותם בקובץ Configuration ב-plaintext.
3.6. תקשורת מבוססת HTTPS, ולא לבטל אימות certificates
פרצה שהוכנסה “רק לזמן הפיתוח” נשארת כמות שהיא בפרודקשן. תקלות תקשורת הן בדרך כלל בדיוק הדפוס הזה.
במיוחד קוד ו-Configuration מהסוג הבא נוטים להישאר במוצר הסופי:
ServicePointManager.ServerCertificateValidationCallback += ... => trueHttpClientHandler.DangerousAcceptAnyServerCertificateValidator- הפצה עם בדיקת revocation של certificates מבוטלת
- קוד שמניח certificate עצמי-חתום לפיתוח, שנשאר בפרודקשן
המדיניות המינימלית פשוטה:
- תקשורת בפרודקשן היא HTTPS
- לא לדלג באופן קבוע על אימות certificates
- אם נדרשת הקלה חריגה באימות, להגביל אותה ל-host ול-certificate ספציפיים
- להסיר קוד עקיפה של פיתוח בוודאות דרך תנאי build או Configuration
- ב-.NET, לשים לב גם לבדיקת revocation
הדוגמה השגויה הטיפוסית נראית כך:
ServicePointManager.ServerCertificateValidationCallback +=
(_, _, _, _) => true;
זה נראה נוח במבט ראשון, אבל בפועל זה קרוב ל”התקשורת הזו ב-HTTPS תתחבר לכל אחד”. כשמסירים אימות certificates, גם אם משתמשים ב-HTTPS, התוכן חלול למדי.
הכתיבה משתנה לפי גרסת .NET
הצבת הגדרה גלובלית ב-ServicePointManager היא סגנון כתיבה מתקופת .NET Framework. בקוד חדש, טבעי יותר לקבל HttpClient מ-IHttpClientFactory, ואם צריך הגדרה סביב TLS, להחזיק אותה בצד SocketsHttpHandler או HttpClientHandler.
עם זאת, מסוכן להזניח בטענה “זה API ישן שכבר לא משפיע”. בתיעוד של Microsoft כתוב ש-ServicePointManager.ServerCertificateValidationCallback מ-.NET 9 ואילך ממופה אל RemoteCertificateValidationCallback של SocketsHttpHandler.SslOptions. כלומר, שורה בודדת של => true איפשהו עלולה להשפיע גם על התקשורת של HttpClient.
flowchart TB
accTitle: המסלול שבו callback ישן משפיע על תקשורת של היום
accDescr: תרשים שמראה שה-ServerCertificateValidationCallback של ServicePointManager ממופה מ-.NET 9 ואילך לאימות של SocketsHttpHandler, ולכן שורה בודדת שמחזירה true עלולה להשפיע גם על תקשורת HttpClient.
old1["callback האימות של ServicePointManager"] -->|"ממופה מ-.NET 9 ואילך"| new1["האימות בצד SocketsHttpHandler"]
new1 --> ef1["משפיע גם על תקשורת HttpClient"]
ef1 -.-> rk2["שורה בודדת שמחזירה true עלולה לחלל את כל התקשורת"]
איור 13: דילוג אימות שהוצב ב-API ישן משפיע, דרך המיפוי, גם על תקשורת HttpClient של היום.
כשרוצים להקל חריגית על האימות, לא כדאי הגדרה גלובלית שמשפיעה על כל ה-process, אלא צורה שמוגבלת רק ל-handler הספציפי.
// C# / .NET 8. דוגמה שמטפלת חריגתית רק במארח ואישור ספציפיים.
// גם אם מקלים לצורך פיתוח, בלי להגביל את היעד זה כמו לבטל את האימות גלובלית.
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
// טביעת האצבע של האישור המצופה. אפשר גם לקרוא מתצורה.
const string ExpectedThumbprint = "2B0C4E6A8D1F3B5D7F9A1C3E5A7C9E1B3D5F7A91";
var handler = new HttpClientHandler
{
CheckCertificateRevocationList = true, // בודקים גם ביטול. ברירת המחדל היא false
ServerCertificateCustomValidationCallback = (request, certificate, chain, errors) =>
{
if (errors == SslPolicyErrors.None)
{
return true;
}
// מקבלים רק את המקרה שבו "המכשיר הזה לא בוטח ב-CA הפנים-ארגוני".
// אישור שלא הגיע או חוסר התאמה בשם מארח — לא מקבלים
if (errors != SslPolicyErrors.RemoteCertificateChainErrors)
{
return false;
}
// מקבעים את הצד השני ואת האישור
if (request.RequestUri?.Host != "device.internal.example"
|| certificate is null
|| !string.Equals(certificate.Thumbprint, ExpectedThumbprint,
StringComparison.OrdinalIgnoreCase))
{
return false;
}
// גם באישור המוצמד לא מקבלים פקיעת תוקף או ביטול.
// ברגע שמקבלים זאת, נפתח מסלול שממשיך להשתמש ב"אישור שבוטל אחרי דליפת מפתח"
return chain is not null
&& chain.ChainStatus.All(s =>
s.Status is X509ChainStatusFlags.NoError
or X509ChainStatusFlags.UntrustedRoot
or X509ChainStatusFlags.PartialChain);
},
};
using var client = new HttpClient(handler);
את ExpectedThumbprint נותנים כקבוע או מתוך Configuration, לפי ה-thumbprint של ה-certificate הרלוונטי.
החשוב כאן הוא לא “להתעלם” מ-errors ומ-chain.ChainStatus. אם מחזירים true רק בגלל שה-thumbprint תואם, זה ימשיך לעבור גם אם ה-certificate פג תוקף, וגם אחרי שהמפתח דלף וגרם ל-revocation. certificate pinning פירושו “מאמינים רק לעותק הזה”, לא “מאמינים לעותק הזה בכל מצב”. בקוד שלמעלה, מה שמותר הוא רק UntrustedRoot ו-PartialChain (כלומר ה-CA הפנימי בארגון לא מותקן במכשיר הזה), בעוד ש-NotTimeValid (פג תוקף) ו-Revoked (בוטל) נדחים ישירות.
כדי לבדוק revocation נדרש CheckCertificateRevocationList = true (ברירת המחדל היא false, ואז ה-revocation לא נבדק). מצד שני, אם ה-CA הפנימי בארגון לא מפרסם לא CRL ולא OCSP, ייחסם עם RevocationStatusUnknown. זו התנהגות נכונה. אם אין אמצעי לבדוק revocation, יש למלא את הפער באחת משתי דרכים: לקצר את תוקף ה-certificate, או להכין מראש מסלול שמאפשר להפיץ מחדש את הערך המוצמד. המצב המסוכן ביותר הוא “pinning של certificate לטווח ארוך בלי אפשרות לבטל אותו”.
flowchart TB
accTitle: זרימת ההחלטה בעת certificate pinning
accDescr: תרשים שמראה שכשאין שגיאה מקבלים, שגיאה שאינה שגיאת שרשרת נדחית, ומתוך שגיאת שרשרת בלבד מאמתים hostname ו-thumbprint, ואף מצב שרשרת של פקיעת תוקף או revocation נדחה.
e0["בדיקת errors"] -->|"None"| pass1["מקבלים"]
e0 -->|"שגיאה שאינה שגיאת שרשרת"| rj1["דוחים"]
e0 -->|"רק שגיאת שרשרת"| hchk["התאמת hostname ו-thumbprint"]
hchk -->|"חוסר התאמה"| rj1
hchk -->|"התאמה"| cchk["בדיקת מצב השרשרת"]
cchk -->|"פג תוקף / בוטל וכדומה"| rj1
cchk -->|"רק CA פנימי בארגון לא מותקן"| pass1
איור 14: גם ב-certificate pinning לא מתעלמים — זרימת ההחלטה דוחה ישירות פקיעת תוקף ו-revocation.
3.7. להתייחס לכל קלט חיצוני כ”קלט לא מהימן”
אפליקציית Windows אינה אפליקציית Web, ולכן validation של קלט נוטה להיות רופף. אבל בפועל, נקודות הכניסה לקלט חיצוני רבות יותר ממה שחושבים.
- נתיבי קבצים
- CSV / Excel / JSON / XML
- command-line arguments
- named pipe / socket / COM / RPC / gRPC
- מחרוזת שמועברת ל-DB
- ערכי Registry
- clipboard
- URL / deep link
- נתונים שחוזרים ממכשיר חיצוני או מ-SDK
בפרט, שלושה דברים שלא כדאי לפספס:
- תמיד לפרמט SQL לא לבנות SQL בשרשור מחרוזות.
- לנרמל נתיב קובץ לפני שימוש לא להשתמש בנתיב שהוזן על ידי המשתמש ישירות למחיקה, דריסה או חילוץ.
- להוסיף מגבלת גודל ובדיקת פורמט בקריאת קובץ חיצוני “נפתח בהצלחה” לא אומר “בטוח”.
בדוגמת SQL, זה מה שכדאי להימנע ממנו:
var sql = "SELECT * FROM Users WHERE Name = '" + userName + "'";
גם ברף המינימלי, כדאי להתקרב לזה:
using System.Data;
using Microsoft.Data.SqlClient;
using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT * FROM Users WHERE Name = @name";
cmd.Parameters.Add("@name", SqlDbType.NVarChar, 256).Value = userName;
ההנחה “כלי פנימי בארגון, אז הקלט מהימן” היא הנחת יסוד מסוכנת למדי. בפועל, CSV שבור, שמות קבצים בלתי צפויים, נתוני DB ישנים, טעויות הקלדה בתפעול, ו-JSON חצי-מוגמר שנכתב על ידי כלי אחר — כל אלה נכנסים כרגיל.
flowchart TB
accTitle: גם לכלי פנימי בארגון מגיע קלט שבור
accDescr: תרשים שמראה שגם בכלי פנימי בארגון נכנס באופן שגרתי CSV שבור, שם קובץ בלתי צפוי, נתוני DB ישנים, טעויות הקלדה ו-JSON חצי-מוגמר, ולכן מתייחסים לכל קלט חיצוני כלא מהימן.
csv1["CSV שבור"] --> in2["קלט אל האפליקציה"]
fn1["שם קובץ בלתי צפוי"] --> in2
hm1["טעות הקלדה / נתונים ישנים"] --> in2
in2 --> tr2["validation כקלט לא מהימן, בכל נקודת כניסה"]
איור 15: גם לכלי פנימי בארגון מגיע קלט שבור כעניין שגרתי, אז עושים validation בכל נקודת כניסה לפני שימוש.
3.8. לא להשאיר את מקור טעינת ה-DLL עמום
זו מלכודת אופיינית ל-Windows.
כשטוענים DLL רק לפי שם, כמו LoadLibrary("foo.dll"), ייתכן שבהתאם לסדר החיפוש ייטען DLL ממקום לא רצוי.
מה שצריך לעשות ידוע מראש:
- כשאפשר, לציין נתיב מוחלט ל-DLL
- להגדיר בשלב מוקדם את
SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS) - להוסיף במפורש יעדי חיפוש עם
AddDllDirectory - להימנע מתכנון שמעביר את תוצאת
SearchPathישירות ל-LoadLibrary - לא להסתמך לחלוטין על safe DLL search mode
לדוגמה, ב-native code, תכנון שמכניס את הבא בשלב מוקדם של אתחול ה-process הוא אפשרות טובה:
SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
ואז רושמים עם AddDllDirectory רק את תיקיות החיפוש הנוספות הנדרשות.
זה נוטה להישאר בלי טיפול כי “בדרך כלל זה עובד”, אבל אם ספריית העבודה משתנה ביעד ההפצה, או ש-DLL של מוצר אחר נכנס ל-PATH, זה נשבר בשקט. זה עוזר לא רק לאבטחה, אלא גם למניעת תקלות.
flowchart TB
accTitle: קיבוע מקור טעינת ה-DLL
accDescr: תרשים שמראה שטעינה לפי שם בלבד עלולה לגרור DLL ממקום לא רצוי לפי סדר החיפוש, ושהגדרה מוקדמת של SetDefaultDllDirectories, הוספת יעד חיפוש עם AddDllDirectory וציון נתיב מוחלט כשאפשר, מונעים עמימות.
nm1["טעינה לפי שם בלבד"] --> pick2["גרירת DLL לא רצוי לפי סדר החיפוש"]
fix1["הגדרה מוקדמת של SetDefaultDllDirectories"] --> add2["הוספה מפורשת עם AddDllDirectory"]
add2 --> abs1["ציון נתיב מוחלט כשאפשר"]
abs1 --> safe1["מקור הטעינה לא נשאר עמום"]
איור 16: מפסיקים להסתמך על שם בלבד, מציינים במפורש יעדי חיפוש, וכך מקבעים את מקור הטעינה.
3.9. לא לחשוף secrets בלוגים וב-exceptions
חשוב להגדיל את הלוגים לצורך חקירת תקלות. אבל הלוג עצמו נוטה גם להיות בית קברות ל-secrets.
סביב הלוגים, כדאי לבחון מחדש לפחות את אלה:
- לא להוציא ללוג סיסמאות, Bearer token, API keys
- לא להוציא connection string מלאה
- למסך מידע אישי ותוכן נתונים עסקי
- להפריד בין פרטי exception למשתמש לבין לוג פנימי
- לא להפעיל בפרודקשן לוג PII שמיועד ל-debug
- לבדוק מחדש הרשאות מיקום שמירה של dump או trace
ב-.NET העדכני, קל יותר גם לסדר את הנושא סביב redaction. לפחות כדאי לוותר על “מייצרים מחרוזת מכל דבר ורושמים כמו שהוא ללוג”.
כמה כשלים אופייניים:
- שמירת גוף request/response של HTTP במלואו
- הוצאת token או כל ה-headers בעת כשלון authentication
- הצגת הודעת exception ישירות ב-MessageBox
- כריכת לוגים רגישים במלואם בקובץ ZIP לתחזוקה
תצוגת שגיאה מתחלקת, למשל, כך:
- למשתמש: “החיבור לשרת נכשל. בדקו את הגדרות הרשת ואת ה-URL.”
- ללוג פנימי: ה-host שנכשל, סוג שגיאת TLS, correlation ID, stack trace, מספר ניסיונות חוזרים
רק ההפרדה הזו כבר משפרת משמעותית את האיזון בין דליפת מידע ליכולת חקירה.
flowchart TB
accTitle: הפרדת תצוגת השגיאה
accDescr: תרשים שמראה שכשמתרחשת שגיאה, למשתמש מוצגת רק הנחיה תמציתית, ואילו ללוג הפנימי נשמר מידע חקירתי כמו ה-host שנכשל, סוג השגיאה, correlation ID ו-stack trace.
er1["התרחשות שגיאה"] --> usr1["למשתמש: הנחיה תמציתית בלבד"]
er1 --> lg2["לוג פנימי: host, סוג, correlation ID וכדומה"]
lg2 -.-> bl1["שמירה על יכולת חקירה בלי לדלוף מידע"]
איור 17: רק הפרדה בין תצוגה למשתמש ללוג פנימי כבר משפרת את האיזון בין דליפה ליכולת חקירה.
3.10. לא להזניח ספריות תלויות וכלי פיתוח
הפריט האחרון צנוע במראה, אבל בעל השפעה גדולה. גם אם גוף האפליקציה נבנה בקפידה, אם ממשיכים להחזיק runtime ישן או ספריית תלות עם פגיעות ידועה, יש חור מתחת לרגליים.
מספר הפריטים לבדיקה עצמו לא גדול.
- לשמור על SDK / runtime של .NET בגרסה נתמכת
- לבדוק תקופתית עדכוני תלויות NuGet / OSS
- ב-C++, לנהל גרסאות של רכיבי הפצה חוזרת של runtime ו-DLL חיצוניים
- לכלול בדיקת מידע פגיעויות בבדיקה שלפני release
- להכין smoke test שלא ייהרס בעדכון תלויות
זו נקודה שבה “נעשה את זה בבת אחת אחר כך” מסוכנת ביותר. אם מזניחים חצי שנה או שנה, פער העדכון גדל מדי, וטיפול האבטחה עצמו הופך לעבודה כבדה.
flowchart TB
accTitle: מה קורה כשמזניחים עדכוני תלויות
accDescr: תרשים שמראה שהזנחת עדכון תלויות למשך חצי שנה עד שנה גורמת לפער עדכון גדול מדי, וטיפול האבטחה עצמו הופך לעבודה כבדה.
pt1["נעשה את זה בבת אחת אחר כך"] --> ac2["הזנחה של חצי שנה עד שנה"]
ac2 --> df1["פער עדכון גדול מדי"]
df1 --> hw1["הטיפול עצמו הופך לעבודה כבדה"]
rg1["בדיקה תקופתית"] -.->|"מונע את זה"| hw1
איור 18: ככל שמזניחים את עדכון התלויות, הפער תופח, וגם הטיפול עצמו הופך לעבודה כבדה.
3.11. איך בודקים כל פריט
Checklist מתפקד רק כשהוא מלווה בשיטת אימות. עבור הפריטים בסעיפים 3.2 עד 3.10, הנה שיטות שאפשר להפעיל בפועל לפני release.
| מה רוצים לבדוק | שיטת בדיקה |
|---|---|
| האם מבקשים elevation שלא לצורך | לבדוק במניפסט של האפליקציה את הערך של requestedExecutionLevel. במקור — app.manifest, בקובץ ההפצה — לבדוק עם Sigcheck של Sysinternals או בעורך משאבים |
| חתימה ו-timestamp | להריץ ב-PowerShell את Get-AuthenticodeSignature .\app.exe ולבדוק שה-Status הוא Valid ושיש TimeStamperCertificate. לבדוק אחד-אחד גם DLL נלווה וגם updater |
| האם ביטלנו אימות certificates | לחפש בכל קוד המקור את ServerCertificateValidationCallback, DangerousAcceptAnyServerCertificateValidator, ServerCertificateCustomValidationCallback, CheckCertificateRevocationList |
| כתיבה ישירה של secrets | לחפש Password=, ApiKey, Secret, Token, ConnectionString. גם קוד נוכחי וגם היסטוריית ה-repository |
| בניית SQL | לחפש שרשור מחרוזות שמכיל "SELECT, "INSERT, + , ולבדוק שהוא עובר דרך Parameters.Add |
| מקור טעינת ה-DLL | ב-Process Monitor, לצמצם ל-process הרלוונטי, ולסנן Path ends with .dll ו-Result is NAME NOT FOUND. כך רואים לאן ובאיזה סדר חיפשו, ובודקים שלא נצפו תיקיות בלתי רצויות |
| פגיעויות ידועות בתלויות | להריץ dotnet list package --vulnerable --include-transitive |
| האם secrets מופיעים בלוג | להריץ פעם אחת, ולחפש בלוג שיוצא Bearer , Password, Authorization |
חיפוש קוד יכול להתבצע עם rg (ripgrep) או עם חיפוש של Visual Studio. להרצה מרוכזת, זו הצורה:
Get-ChildItem -Recurse -Include *.cs,*.vb,*.cpp,*.h,*.config,*.json |
Select-String -Pattern 'ServerCertificateValidationCallback|DangerousAcceptAnyServerCertificateValidator|Password=|ApiKey' |
Select-Object Path, LineNumber, Line
חשוב לתעד את עצם הבדיקה. אם משאירים תיעוד גם של “בוצע חיפוש” וגם של “0 תוצאות”, ב-release הבא צריך לבדוק רק את ההפרש.
flowchart TB
accTitle: התועלת בתיעוד הבדיקה
accDescr: תרשים שמראה שכשמתעדים בדיקה שלפני release גם כאשר התוצאה היא 0 תוצאות, ב-release הבא צריך לבדוק רק את ההפרש.
chk2["ביצוע בדיקה לפני release"] --> rec1["תיעוד: בוצע חיפוש · 0 תוצאות"]
rec1 --> nx1["ב-release הבא מספיק לבדוק רק הפרש"]
איור 19: תיעוד עצם “הבדיקה שבוצעה” חוסך ב-release הבא — מספיק לבדוק את ההפרש.
4. Checklist שלפני release
בצורה שאפשר להשתמש בה ישירות כתבנית ל-review או להחלטת release. כדי שיהיה נוח לבדוק על טבלה, מסדרים לפי קטגוריה את הפריטים שכדאי לבדוק כמינימום לפני release.
4.1. הרשאות, אופן הרצה
| פריט לבדיקה | בדיקה | הערה |
|---|---|---|
ההרצה הרגילה פועלת ב-asInvoker |
□ | |
| עבודה שדורשת הרשאות Administrator מופרדת ל-EXE נפרד / Windows Service וכדומה | □ | |
| אם משתמשים ב-service, חשבון ההרצה אינו חזק מהנדרש | □ | |
האחריות מופרדת בין %ProgramFiles% לנתוני משתמש |
□ |
4.2. הפצה, חתימה
| פריט לבדיקה | בדיקה | הערה |
|---|---|---|
| EXE / DLL / MSI / MSIX / updater חתומים | □ | |
| החתימה כוללת timestamp | □ | |
| פקיעת תוקף ה-certificate והליך החידוש נכללים בזרימת ה-release | □ | |
| נקבעה שיטה לאימות hash ולזיהוי שיבוש בקובצי ההפצה | □ |
4.3. update
| פריט לבדיקה | בדיקה | הערה |
|---|---|---|
| קבלת ה-update מתבצעת ב-HTTPS | □ | |
| אחרי הורדה מתבצע אימות חתימה או hash | □ | |
| התכנון מקשה על החלפה שרירותית של URL מקור ה-update | □ | |
| קיימת מדיניות rollback או ניסיון חוזר במקרה כשלון update | □ |
4.4. secrets
| פריט לבדיקה | בדיקה | הערה |
|---|---|---|
| סיסמה, API key, connection string אינם כתובים ישירות בקוד המקור | □ | |
| לא נשמר secret בקובץ Configuration ב-plaintext | □ | |
| secret שנדרש לשמירה מקומית מוגן ב-DPAPI / Credential Locker וכדומה | □ | |
| היכן שאפשר, נעשה שימוש ב-Windows authentication או ב-credentials של המשתמש | □ |
4.5. תקשורת
| פריט לבדיקה | בדיקה | הערה |
|---|---|---|
| תקשורת בפרודקשן משתמשת ב-HTTPS | □ | |
DangerousAcceptAnyServerCertificateValidator או => true לא נשארו בקובץ ההפצה |
□ | |
| בדיקת revocation ואימות hostname נלקחים בחשבון | □ | |
| קוד או Configuration שמניחים certificate פיתוח לא הוזנחו בפרודקשן | □ |
4.6. קלט, גישה לנתונים
| פריט לבדיקה | בדיקה | הערה |
|---|---|---|
| SQL מפורמט | □ | |
| לקלט מ-command-line, קובץ, IPC, URI וכדומה יש מגבלה ובדיקת פורמט | □ | |
| טיפול בנתיבים מנורמל ומונע חריגה מהשורש | □ | |
| הודעת exception לא מוצגת ישירות למסך | □ |
4.7. DLL וסביבת הרצה
| פריט לבדיקה | בדיקה | הערה |
|---|---|---|
| מקור טעינת ה-DLL מוגדר במפורש | □ | |
סדר החיפוש נשלט עם SetDefaultDllDirectories / AddDllDirectory וכדומה |
□ | |
| טעינת DLL לא מסתמכת על הספרייה הנוכחית או על PATH | □ | |
| ידוע אילו קבצים דרושים לטעינה דינמית ביעד ההפצה | □ |
4.8. לוגים, תפעול
| פריט לבדיקה | בדיקה | הערה |
|---|---|---|
| tokens, סיסמאות, PII אינם מופיעים בלוג | □ | |
| לוג פנימי מופרד מהודעה למשתמש | □ | |
| הרשאות מיקום שמירה של dump / trace / log נבדקו מחדש | □ | |
| מצב העדכון של SDK וספריות תלויות נבדק | □ |
5. הנחות שגויות נפוצות
בעבודה מעשית נתקלים בדרך כלל בהנחות שגויות מהסוג הזה.
5.1. “זה כלי פנימי בארגון אז זה בסדר”
גם בכלי פנימי בארגון, קבצים שבורים, תפעול שגוי, מכשירים חיצוניים, תיקיות משותפות, DLL ישן והגדרות הרשאה רשלניות — כל אלה מתקיימים כרגיל. גם ללא פרסום לאינטרנט, משטח התקיפה לא נעלם.
5.2. “זה HTTPS אז זה בטוח”
HTTPS חשוב, אבל ביטול אימות certificates מרוקן את זה ממשמעות במידה רבה. בנוסף, בהפצת updates לא מספיק HTTPS בלבד — נדרש גם אימות אמיתות קובץ ההפצה.
5.3. “הצפנו אז זה בטוח”
בלי לסגור את מיקום מפתח הפענוח, הרשאת הפענוח, גבול המשתמש וגבול המכונה, ההצפנה לבדה לא מספיקה.
בפרט, אם משתמשים בערך שמוגן ב-LocalMachine בהנחה שהוא “secret per-user”, זה יגרום בהמשך לבלבול.
5.4. “אם נגדיל את הלוגים נוכל לחקור”
אם הלוגים רבים אבל tokens ומידע אישי זולגים בהם, זו כשלעצמה תקרית. אם רוצים יכולת חקירה, קודם כדאי לקבוע מה נשמר ומה מוסתר.
5.5. “אם נריץ כ-Administrator זה ייפתר”
זה נוח בהתחלה, אבל בהמשך זה בדרך כלל מקשה סביב UAC, הפצה, תמיכה, גבולות הרשאה, טעינת DLL ומיקום שמירת קבצים. הרשאה מינימלית יציבה יותר לטווח הארוך.
flowchart TB
accTitle: הסוף של "אם נריץ כ-Administrator זה ייפתר"
accDescr: תרשים שמראה שהרצה קבועה בהרשאות Administrator נוחה בהתחלה אך מקשה בהמשך סביב UAC, הפצה, תמיכה, גבולות הרשאה, טעינת DLL ומיקום שמירת קבצים, ושהרשאה מינימלית יציבה יותר לטווח הארוך.
ez1["אם נריץ כ-Administrator זה ייפתר"] --> ez2["נוח בהתחלה"]
ez2 --> pain1["מקשה סביב UAC, הפצה ותמיכה"]
ez2 --> pain2["מקשה סביב גבולות הרשאה ומיקום שמירה"]
lp1["הרצה בהרשאה מינימלית"] -->|"יציבה יותר לטווח הארוך"| ok2["נמנעים מהקושי"]
איור 20: הנוחות בהרצה קבועה כ-Administrator היא רק בהתחלה, ולטווח הארוך הרשאה מינימלית יציבה יותר.
6. סדר עדיפויות כללי
אם קשה לעשות הכול בבת אחת, סדר העדיפויות נראה בערך כך.
הבסיס לסידור הוא מכפלה בין גודל הנזק במקרה תקרית לבין נמוכות עלות התיקון. פרק 3 מסודר לפי זרימת התכנון “הרשאות ← הפצה ← מימוש ← תפעול”, אבל כאן הסדר הוא “מהמסוכן ביותר”, ולכן הוא לא זהה לסדר הפרקים. מצורף הסעיף המתאים לכל פריט.
- בחינה מחדש של הרשאות Administrator (3.2)
קודם כל, להפסיק את השימוש הקבוע ב-
requireAdministrator. היקף הנזק משתנה בצעד אחד, בעוד שינוי התכנון עצמו לרוב קטן. - חתימה ו-timestamp (3.3) לסגור את אמינות קבצי ההפצה. מספיק לשלב בהליך, ואם נדחה לאחר מכן נדרשת הפצה מחדש.
- הוצאת secrets (3.5) להוציא secret מקוד המקור ומ-Configuration ב-plaintext. הנזק בעת דליפה גדול, ואחרי הדליפה אין דרך חזרה.
- תיקון HTTPS + אימות certificates (3.6)
להסיר מקובצי ההפצה סוגים של
=> true. לרוב מספיקה הסרה בלבד, ובהזנחה כל התקשורת בלתי מהימנה. - בחינה מחדש של קלט SQL / קובץ / IPC (3.7) לצמצם שרשור מחרוזות וקלט לא מאומת. הכמות רבה, אך אפשר לתקן מקום אחד בכל פעם.
- קיבוע טעינת DLL (3.8) להפסיק טעינה לפי שם בלבד והסתמכות על PATH. זה תיקון של חלק מתהליך ההפעלה, ומועיל גם למניעת תקלות.
- מיסוך לוגים (3.9) לוודא שהלוג לא נהיה אסון משני בעת תקרית. יש הרבה מקומות פלט, ולכן זה לוקח זמן.
- קביעות של עדכון תלויות (3.10) להפוך לתהליך קבוע שנבדק בכל release. לא נגמר בפעם אחת — העבודה היא להפוך את זה למנגנון.
מסלול ה-update (3.4) לא נכלל בסדר הזה כי לא לכל אפליקציה יש update אוטומטי. אם יש, כדאי לראות אותו באותה עדיפות כמו פריט 2, החתימה. מודול ה-update נמצא במעמד חלש יותר מגוף האפליקציה, אך יש בכוחו לשכתב אותו.
flowchart TB
accTitle: איך קובעים סדר עדיפויות
accDescr: תרשים שמראה שהסדר נקבע לפי מכפלה של גודל הנזק ונמוכות עלות התיקון, מהמסוכן ביותר, ושבאפליקציה עם update אוטומטי מסלול ה-update נבחן באותה עדיפות כמו החתימה.
dmg1["גודל הנזק"] --> mul1["מכפלה שקובעת את הסדר"]
cst1["נמוכות עלות התיקון"] --> mul1
mul1 --> ord1["סגירה לפי סדר מהמסוכן"]
ord1 -.-> upn1["באפליקציה עם update אוטומטי — מסלול ה-update באותה עדיפות כמו החתימה"]
איור 21: סדר העדיפויות נקבע ממכפלת גודל הנזק ועלות התיקון, וסוגרים מהחור המסוכן ביותר.
בסדר הזה, נוח להתקדם במובן של “קודם לסגור את החורים שברור שהם מסוכנים”.
7. סיכום
אבטחת פיתוח אפליקציות Windows משתנה משמעותית כבר רק מסגירת שבע הנקודות הרשאות, חתימה, secrets, תקשורת, קלט, DLL ולוגים — עוד לפני הכנסת מוצר מיוחד או מנגנון ענק.
הרף המינימלי, במשפט אחד לכל נקודה:
- לא להריץ את כל האפליקציה בהרשאות Administrator
- לחתום על קבצי ההפצה וה-update, כולל timestamp
- לא להחזיק secrets בקוד המקור או ב-Configuration ב-plaintext
- גם עם HTTPS, לא לבטל אימות certificates
- לא לתת אמון בקלט חיצוני כמו SQL, קובץ, IPC וכדומה
- לא להשאיר את מקור טעינת ה-DLL עמום
- לא לחשוף secrets בלוגים
- לא להזניח ספריות תלויות
נושא האבטחה רחב, אבל לא חייבים לעשות הכול מההתחלה. עם זאת, שווה להשלים כבר בשלב מוקדם למדי את המינימום של לא להפיץ התנהגות default מסוכנת כמו שהיא.
8. מקורות
- Administrator Broker Model - Win32 apps
- How User Account Control works
- Authenticode Digital Signatures
- Time Stamping Authenticode Signatures
- Sign a Windows app package
- Credential Locker for Windows apps
- CryptProtectData function (dpapi.h)
- CA5359: Do not disable certificate validation
- CA5399: Enable HttpClient certificate revocation list check
- Configuring parameters - ADO.NET Provider for SQL Server
- Connection String Syntax - ADO.NET
- Dynamic-Link Library Security - Win32 apps
- SetDefaultDllDirectories function (libloaderapi.h)
- Data redaction in .NET
- ProtectedData Class - הבהרה שמדובר במיועד ל-Windows בלבד, וכן אזהרה לגבי מצב שבו הפרופיל לא טעון.
- ServicePointManager.ServerCertificateValidationCallback - מ-.NET 9 ואילך ממופה להגדרות של SocketsHttpHandler.
- dotnet list package command - עם
--vulnerableאפשר לבדוק פגיעויות ידועות. - Get-AuthenticodeSignature
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
UAC ב-Windows: איך מפרידים רק פעולות שדורשות Administrator
באפליקציית Windows משאירים את ה-UI ב-asInvoker ומפרידים רק פעולות שדורשות הרשאות Administrator ל-helper EXE. המאמר עובר בפירוט על UAC, ru...
Secrets ביישומי Windows: DPAPI במקום plaintext ב-config
איך לא לשמור connection strings ו-API tokens ב-plaintext בקובץ config של יישום Windows. המאמר עובר על DPAPI / ProtectedData, על ההבדל בין...
Named Pipes בפועל — ה-IPC הסטנדרטי של Windows, מתכנון עד אבטחה
מדריך מעשי ל-Named Pipes, ה-IPC הסטנדרטי ב-Windows. המאמר מסדר לפי מקורות ראשוניים את הבחירה בין byte mode ל-message mode, תכנון server ל...
Exception לא צפוי ב-.NET: מתי מסיימים תהליך ומתי ממשיכים
מתי מסיימים אפליקציית Windows אחרי exception לא צפוי ומתי אפשר להמשיך: לפי state corruption, side effect חיצוני, threads וגבול native.
הרשת עובדת אבל Windows מציג "אין אינטרנט" — לבודד NCSI, DNS, Proxy ו-VPN ב-Windows
למה Windows מציג "אין אינטרנט" בזמן שהרשת עובדת — נקודת המוצא היא קביעת הקישוריות של NCSI. כאן מפרידים בין DNS, proxy, VPN ו-captive port...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח יישומי Windows
זה נושא של בחינה מחדש של אפליקציית Windows שלמה — תכנון הרשאות, שיטת הפצה, שיטת update ותכנון לוגים — ולכן הוא מתאים ל-Windows Custom Software Development.
ייעוץ טכני וסקירת תכנון
אם רוצים להתחיל מבחינה מחדש של האבטחה באפליקציה קיימת, מגבולות ההרשאות או מתכנון מחדש של מדיניות ה-updater, אפשר להתקדם בייעוץ טכני ו-design review.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- מה כדאי לבדוק ראשון באבטחת אפליקציות Windows?
- ארבעה דברים: לא לדרוש הרשאות Administrator שלא לצורך, לחתום על הקוד, לא להחזיק secrets ב-plaintext, ולא לבטל אימות certificates. אבטחה מינימלית אינה הוספת features מיוחדים אלא הימנעות מהשארת התנהגות default מסוכנת או מימוש רשלני. דילוג קבוע על אימות certificates, connection strings ב-plaintext, טעינת DLL שמסתמכת על הספרייה הנוכחית, והרצת SQL בשרשור מחרוזות — אלה דברים שכדאי להימנע מהם גם ברף המינימלי.
- אסור להריץ את כל האפליקציה בהרשאות Administrator?
- כדאי להימנע מכך. הרצת כל האפליקציה בהרשאות Administrator גורמת לכך שבאגים, החלפת DLL, קריאה שגויה של קבצי Configuration ופגמים בקלט חיצוני רצים ישירות בהרשאה חזקה. המדיניות הבסיסית היא שאפליקציית UI רגילה רצה ב-asInvoker, שרק ה-processes שדורשים הרשאות Administrator מופרדים ל-process נפרד או ל-Windows Service, ושההעלאה בהרשאה מתבצעת רק ברגע הנדרש.
- איפה כדאי לשמור API keys ו-connection strings?
- קודם יוצאים ממצב שבו הם יושבים ב-plaintext בקוד המקור או בקבצי Configuration כמו appsettings.json. secrets שנשמרים מקומית משתמשים לפי הצורך ב-DPAPI / ProtectedData או ב-Credential Locker. חשוב גם למסך בלוגים tokens, סיסמאות, connection strings ומידע אישי, כי אחרת הלוג עצמו נהיה גורם התקרית.
- אפליקציה שמופצת פנימית בארגון גם צריכה code signing?
- כדאי להתייחס לזה כהנחת יסוד. באפליקציות Windows, קבצי ההפצה עצמם (EXE / DLL / MSI / MSIX / מודול update אוטומטי) הם משטח תקיפה. code signing עם timestamp נותן זיהוי שיבוש, אמינות בעיני המשתמשים, ונוחות בהסבר תפעולי. גם מנגנון ה-update צריך לקבע את מקור ה-update, ולזהות שיבוש דרך HTTPS ואימות חתימה.