הזרקת SQL וכיצד ניתן למנוע זאת?

עליון התמדה

רק הכרזתי על החדש למד אביב קורס, המתמקד ביסודות האביב 5 ומגף האביב 2:

>> בדוק את הקורס

1. הקדמה

למרות היותה אחת הפגיעות המוכרות ביותר, SQL Injection ממשיכה להיות במקום הראשון ברשימת ה- OWASP המפורסמת המובילה של OWASP - כעת חלק מהרשימה הכללית יותר זריקה מעמד.

במדריך זה נחקור טעויות קידוד נפוצות ב- Java המובילות ליישום פגיע וכיצד להימנע מהן באמצעות ממשקי ה- API הזמינים בספריית זמן הריצה הסטנדרטית של JVM. אנו נסקור גם אילו הגנות נוכל להוציא מ- ORM כמו JPA, Hibernate ואילו ואילו נקודות עיוורות נצטרך לדאוג.

2. כיצד יישומים הופכים לפגיעים בהזרקת SQL?

התקפות הזרקה עובדות מכיוון שבשביל יישומים רבים הדרך היחידה לבצע חישוב נתון היא ליצור דינמי קוד שמופעל על ידי מערכת או רכיב אחר.. אם בתהליך יצירת קוד זה אנו משתמשים בנתונים לא מהימנים ללא חיטוי מתאים, אנו משאירים דלת פתוחה לנצל את האקרים.

הצהרה זו עשויה להישמע מעט מופשטת, אז בואו נסתכל כיצד זה קורה בפועל עם דוגמא לספר לימוד:

רשימה ציבורית unsafeFindAccountsByCustomerId (מחרוזת customerId) זורק SQLException {// UNSAFE !!! אל תעשה זאת !!! מחרוזת sql = "בחר" + "customer_id, acc_number, branch_id, יתרה" + "מתוך חשבונות שבהם customer_id = '" + customerId + "'"; חיבור c = dataSource.getConnection (); ResultSet rs = c.createStatement (). ExecuteQuery (sql); // ...}

הבעיה בקוד זה ברורה: שמנו ה מספר לקוחערך השאילתה ללא אימות כלל. שום דבר רע לא יקרה אם אנו בטוחים שערך זה יגיע רק ממקורות מהימנים, אך האם כן?

בואו נדמיין כי פונקציה זו משמשת ביישום REST API עבור חֶשְׁבּוֹן מַשׁאָב. ניצול קוד זה הוא טריוויאלי: כל שעלינו לעשות הוא לשלוח ערך שכאשר משורשר לחלק הקבוע של השאילתה, ישנה את התנהגותו המיועדת:

תלתל -X GET \ '// localhost: 8080 / חשבונות? customerId = abc% 27% 20 או% 20% 271% 27 =% 271' \

בהנחה שה- מספר לקוח ערך הפרמטר לא מסומן עד שהוא מגיע לתפקיד שלנו, הנה מה שנקבל:

abc 'או' 1 '=' 1

כאשר אנו מצטרפים לערך זה עם החלק הקבוע, נקבל את משפט SQL הסופי שיבוצע:

בחר customer_id, acc_number, branch_id, יתרה מחשבונות שבהם customerId = 'abc' או '1' = '1'

כנראה לא מה שרצינו ...

מפתח חכם (לא כולנו?) היה חושב עכשיו: “זה טיפשי! תְעוּדַת זֶהוּת לעולם לא השתמש בשרשור מחרוזות לבניית שאילתה כזו ".

לא כל כך מהר ... הדוגמה הקנונית הזו היא באמת מטופשת אבל ישנם מצבים שאולי עדיין נצטרך לעשות זאת:

  • שאילתות מורכבות עם קריטריוני חיפוש דינמיים: הוספת סעיפי UNION בהתאם לקריטריונים המסופקים על ידי המשתמש
  • קיבוץ או הזמנה דינמיים: ממשקי API של REST המשמשים כ- backend לטבלת נתונים של GUI

2.1. אני משתמש ב- JPA. אני בטוח, נכון?

זו תפיסה מוטעית נפוצה. JPA ו- ORM אחרים פוטרים מאיתנו ליצור הצהרות SQL מקודדות ביד, אך הן לא ימנע מאיתנו לכתוב קוד פגיע.

בואו נראה איך נראית גרסת JPA של הדוגמה הקודמת:

רשימה ציבורית unsafeJpaFindAccountsByCustomerId (String customerId) {String jql = "from Account where customerId = '" + customerId + "'"; TypedQuery q = em.createQuery (jql, Account.class); להחזיר q.getResultList () .stream () .map (זה :: toAccountDTO) .collect (Collectors.toList ()); } 

אותה נושא שהצבענו בעבר קיים גם כאן: אנו משתמשים בקלט לא מאומת ליצירת שאילתת JPAאז אנו נחשפים לאותו סוג ניצול כאן.

3. טכניקות מניעה

כעת, כשאנו יודעים מהי הזרקת SQL, בואו נראה כיצד אנו יכולים להגן על הקוד שלנו מפני התקפה מסוג זה. כאן אנו מתמקדים בכמה טכניקות יעילות מאוד הקיימות ב- Java ובשפות JVM אחרות, אך מושגים דומים זמינים לסביבות אחרות, כגון PHP, .Net, Ruby וכן הלאה.

למי שמחפש רשימה מלאה של טכניקות זמינות, כולל ספציפיות למסדי נתונים, פרויקט OWASP מקיים גיליון צ'יטים למניעת הזרקת SQL, שהוא מקום טוב ללמוד עוד על הנושא.

3.1. שאילתות פרמטריות

טכניקה זו כוללת שימוש בהצהרות מוכנות עם מציין המיקום של סימן השאלה ("?") בשאילתות שלנו בכל פעם שאנחנו צריכים להוסיף ערך המסופק על ידי המשתמש. זה יעיל מאוד, אלא אם כן יש באג ביישום מנהל ההתקן JDBC, חסין בפני מעללים.

בוא נשכתב את פונקציית הדוגמה שלנו לשימוש בטכניקה זו:

public List safeFindAccountsByCustomerId (String customerId) זורק חריג {String sql = "select" + "customer_id, acc_number, branch_id, balance from Accounts" + "where customer_id =?"; חיבור c = dataSource.getConnection (); PreparedStatement p = c.prepareStatement (sql); p.setString (1, customerId); ResultSet rs = p.executeQuery (sql)); // הושמט - עיבוד שורות והחזרת רשימת חשבונות}

הנה השתמשנו ב- preparStatement () שיטה זמינה ב- חיבור למשל כדי להשיג הצהרה מוכנה. ממשק זה מרחיב את הרגיל הַצהָרָה ממשק למספר שיטות המאפשרות לנו להוסיף בבטחה ערכים המסופקים על ידי המשתמש בשאילתה לפני ביצועו.

עבור JPA, יש לנו תכונה דומה:

מחרוזת jql = "מחשבון שבו customerId =: customerId"; TypedQuery q = em.createQuery (jql, Account.class) .setParameter ("customerId", customerId); // ביצוע שאילתה והחזרת תוצאות ממופות (הושמט)

בעת הפעלת קוד זה תחת Spring Boot, אנו יכולים להגדיר את המאפיין logging.level.sql כדי DEBUG ולראות איזו שאילתה נבנית בפועל על מנת לבצע פעולה זו:

// הערה: פלט מעוצב כך שיתאים למסך [DEBUG] [SQL] בחר חשבון0_.id כ- id1_0_, account0_.acc_number כמו acc_numb2_0_, account0_. יתרה כיתרה 3_0_, account0_.branch_id כ- branch_i4_0_, account0_.customer_id כ- client5_0_ חשבון .customer_id =?

כצפוי, שכבת ה- ORM יוצרת הצהרה מוכנה באמצעות מציין מיקום עבור ה- מספר לקוח פָּרָמֶטֶר. זה אותו הדבר שעשינו במקרה JDBC הרגיל - אבל עם כמה הצהרות פחות, וזה נחמד.

כבונוס, גישה זו מביאה בדרך כלל לשאילתה עם ביצועים טובים יותר, מכיוון שרוב מסדי הנתונים יכולים לשמור במטמון את תוכנית השאילתה המשויכת להצהרה מוכנה.

שימו לב כי גישה זו פועלת רק עבור מצייני מיקום המשמשים כערכים. למשל, איננו יכולים להשתמש במצייני מיקום כדי לשנות באופן דינמי את שם הטבלה:

// זה לא יעבוד !!! PreparedStatement p = c.prepareStatement ("בחר ספירה (*) מ?"); p.setString (1, tableName);

כאן, JPA לא יעזור גם:

// זה לא יעבוד אף אחד !!! מחרוזת jql = "בחר ספירה (*) מ: tableName"; TypedQuery q = em.createQuery (jql, Long.class) .setParameter ("tableName", tableName); להחזיר q.getSingleResult (); 

בשני המקרים נקבל שגיאת זמן ריצה.

הסיבה העיקרית מאחורי זה היא עצם האמירה המוכנה: שרתי מסדי נתונים משתמשים בהם במטמון במטמון את תוכנית השאילתה הנדרשת כדי למשוך את קבוצת התוצאות, שלרוב זהה לכל ערך אפשרי. זה לא נכון לגבי שמות טבלאות ומבנים אחרים הזמינים בשפת SQL כגון עמודות המשמשות ב- מיין לפי סָעִיף.

3.2. JPA Criteria API

מכיוון שבניית שאילתות JQL מפורשות היא המקור העיקרי להזרקות SQL, עלינו להעדיף את השימוש ב- API של השאלות של JPA, במידת האפשר.

לקבלת פריימר מהיר ב- API זה, עיין במאמר על שאילתות קריטריונים למצב שינה. כדאי לקרוא גם את המאמר שלנו על JPA Metamodel, שמראה כיצד ליצור מחלקות metamodel שיעזרו לנו להיפטר מקביעות מחרוזות המשמשות לשמות עמודות - ובאגי זמן הריצה שנוצרים כשהם משתנים.

בוא נשכתב את שיטת שאילתת JPA שלנו לשימוש ב- API של קריטריונים:

CriteriaBuilder cb = em.getCriteriaBuilder (); CriteriaQuery cq = cb.createQuery (Account.class); שורש שורש = cq.from (Account.class); cq.select (root) .where (cb.equal (root.get (Account_.customerId), customerId)); TypedQuery q = em.createQuery (cq); // ביצוע שאילתה והחזרת תוצאות ממופות (הושמט)

כאן השתמשנו בשורות קוד נוספות בכדי להשיג את אותה התוצאה, אך המהפך הוא שכעת אנחנו לא צריכים לדאוג לתחביר JQL.

נקודה חשובה נוספת: למרות מילוליותה, ממשק ה- API של קריטריונים הופך את יצירת שירותי השאילתות המורכבים ליותר פשוטים ובטוחים יותר. לקבלת דוגמה מלאה המראה כיצד לעשות זאת בפועל, אנא התבונן בגישה בה משתמשים יישומים שנוצרו על ידי JHipster.

3.3. חיטוי נתוני משתמשים

חיטוי נתונים היא טכניקה של החלת מסנן על הנתונים המסופקים על ידי המשתמש, כך שניתן להשתמש בהם בבטחה על ידי חלקים אחרים ביישום שלנו. יישום המסנן עשוי להשתנות מאוד, אך בדרך כלל אנו יכולים לסווג אותם בשני סוגים: רשימות לבנות ורשימות שחורות.

רשימות שחורות, המורכבים ממסננים המנסים לזהות דפוס לא חוקי, הם בדרך כלל בעלי ערך מועט בהקשר למניעת הזרקת SQL - אך לא לזיהוי! עוד על כך בהמשך.

רשימות הלבנה, לעומת זאת, עובדים טוב במיוחד כאשר אנו יכולים להגדיר בדיוק מהו קלט חוקי.

בואו נשפר את שלנו safeFindAccountsByCustomerId השיטה כך שעכשיו המתקשר יכול גם לציין את העמודה המשמשת למיון מערך התוצאות. מכיוון שאנו מכירים את מערך העמודות האפשריות, אנו יכולים ליישם רשימת היתרים באמצעות קבוצה פשוטה ולהשתמש בה לניקוי הפרמטר שהתקבל:

סופי סטטי פרטי סט VALID_COLUMNS_FOR_ORDER_BY = Collections.unmodifiableSet (זרם. of ("acc_number", "branch_id", "balance") .collect (Collectors.toCollection (HashSet :: new))); public List safeFindAccountsByCustomerId (String customerId, String orderBy) מעביר חריג {String sql = "בחר" + "customer_id, acc_number, branch_id, יתרה מחשבונות" + "איפה customer_id =?"; אם (VALID_COLUMNS_FOR_ORDER_BY.contains (orderBy)) {sql = sql + "סדר לפי" + orderBy; } אחר {לזרוק IllegalArgumentException ("נסה יפה!"); } חיבור c = dataSource.getConnection (); PreparedStatement p = c.prepareStatement (sql); p.setString (1, customerId); // ... עיבוד קבוצת התוצאות הושמט}

פה, אנו משלבים בין גישת ההצהרה המוכנה לבין רשימת היתרים המשמשת לחיטוי מיין לפי טַעֲנָה. התוצאה הסופית היא מחרוזת בטוחה עם משפט SQL הסופי. בדוגמה פשוטה זו, אנו משתמשים בערכה סטטית, אך יכולנו גם להשתמש בפונקציות מטא נתונים של מסד נתונים כדי ליצור אותה.

אנו יכולים להשתמש באותה גישה עבור JPA, וגם לנצל את ממשק ה- API והמטא-נתונים של Criteria כדי להימנע משימוש חוּט קבועים בקוד שלנו:

// מפה של עמודות JPA תקפות למיון מפה סופית VALID_JPA_COLUMNS_FOR_ORDER_BY = Stream.of (AbstractMap.SimpleEntry חדש (Account_.ACC_NUMBER, Account_.accNumber), AbstractMap.SimpleEntry חדש (Account_.BRANCH_ID, Account_.branchId), AbstractMap.SimpleEntry חדש (חשבון_מאזן). (Collectors.toMap (Map.Entry :: getKey, Map.Entry :: getValue)); SingularAttribute orderByAttribute = VALID_JPA_COLUMNS_FOR_ORDER_BY.get (orderBy); אם (orderByAttribute == null) {זרוק IllegalArgumentException ("נחמד לנסות!"); } CriteriaBuilder cb = em.getCriteriaBuilder (); CriteriaQuery cq = cb.createQuery (Account.class); שורש שורש = cq.from (Account.class); cq.select (root) .where (cb.equal (root.get (Account_.customerId), customerId)) .orderBy (cb.asc (root.get (orderByAttribute))); TypedQuery q = em.createQuery (cq); // ביצוע שאילתה והחזרת תוצאות ממופות (הושמט)

לקוד זה יש אותו מבנה בסיסי כמו ב- JDBC הרגיל. ראשית, אנו משתמשים ברשימת היתרים כדי לחטא את שם העמודה, ואז נמשיך ליצור a CriteriaQuery להביא את הרשומות ממסד הנתונים.

3.4. האם אנחנו בטוחים עכשיו?

נניח שהשתמשנו בשאילתות ו / או רשימות היתרים בכל מקום. האם אנו יכולים כעת ללכת למנהל שלנו ולהבטיח שאנחנו בטוחים?

ובכן ... לא כל כך מהר. מבלי לשקול אפילו את בעיית העצירה של טיורינג, ישנם היבטים נוספים שעלינו לקחת בחשבון:

  1. פרוצדורות מאוחסנות: אלה מועדים גם לבעיות הזרקת SQL; במידת האפשר אנא החל תברואה גם על ערכים אשר יישלחו למסד הנתונים באמצעות הצהרות מוכנות
  2. טריגרים: אותה בעיה כמו בשיחות נוהל, אבל אפילו יותר ערמומי כי לפעמים אין לנו מושג שהם שם ...
  3. הפניות לאובייקט ישיר לא בטוח: גם אם היישום שלנו הוא ללא הזרקת SQL, עדיין קיים סיכון שקשורים לקטגוריית פגיעות זו - הנקודה העיקרית כאן קשורה לדרכים שונות שתוקף יכול להערים על היישום, ולכן היא מחזירה רשומות שלא היה אמור להיות לה גישה ל - יש גיליון רמאות טוב בנושא זה במאגר GitHub של OWASP

בקיצור, האפשרות הטובה ביותר שלנו כאן היא זהירות. ארגונים רבים כיום משתמשים ב"צוות אדום "בדיוק לשם כך. תנו להם לעשות את העבודה שלהם, וזה בדיוק למצוא את כל הפגיעות שנותרו.

4. טכניקות לבקרת נזקים

כנוהג אבטחה טוב, עלינו תמיד ליישם מספר שכבות הגנה - מושג המכונה הגנה בעומק. הרעיון העיקרי הוא שגם אם איננו מצליחים למצוא את כל הפגיעות האפשריות בקוד שלנו - תרחיש נפוץ בעת התמודדות עם מערכות מדור קודם - עלינו לפחות לנסות להגביל את הנזק שתקיפה תגרום לו.

כמובן, זה יהיה נושא למאמר שלם או אפילו לספר, אבל בואו נקרא כמה מדדים:

  1. החל את עיקרון הזכות הפחותה: הגבל ככל האפשר את ההרשאות של החשבון המשמשות לגישה למסד הנתונים
  2. השתמש בשיטות ספציפיות למסד נתונים הזמינות על מנת להוסיף שכבת הגנה נוספת; לדוגמא, למסד H2 יש אפשרות ברמת הפעלה המבטלת את כל הערכים המילוליים בשאילתות SQL
  3. השתמש בתעודות קצרות מועד: הפוך את היישום לסיבוב אישורי מסד נתונים לעיתים קרובות; דרך טובה ליישם זאת היא באמצעות Spring Cloud Vault
  4. רשום הכל: אם היישום שומר נתוני לקוחות, זה חובה; ישנם פתרונות רבים הזמינים המשתלבים ישירות במסד הנתונים או עובדים כ- proxy, כך שבמקרה של התקפה נוכל לפחות להעריך את הנזק
  5. השתמש ב- WAF או בפתרונות דומים לאיתור פריצות: אלה הם האופייניים רשימה שחורה דוגמאות - בדרך כלל, הם מגיעים עם מסד נתונים גדול של חתימות התקפה ידועות ויגרמו לפעולה הניתנת לתכנות עם הגילוי. חלקם כוללים גם סוכני JVM שיכולים לזהות חדירות על ידי יישום מכשור כלשהו - היתרון העיקרי של גישה זו הוא שפגיעות בסופו של דבר הופכת להרבה יותר קלה לתיקון מכיוון שיהיה לנו עקבות מלא של מחסנית.

5. מסקנה

במאמר זה סקרנו את פרצות ה- SQL Injection ביישומי Java - איום חמור מאוד על כל ארגון שתלוי בנתונים לעסק שלהם - וכיצד למנוע אותם באמצעות טכניקות פשוטות.

כרגיל, קוד מלא למאמר זה זמין ב- Github.

תחתית התמדה

רק הכרזתי על החדש למד אביב קורס, המתמקד ביסודות האביב 5 ומגף האביב 2:

>> בדוק את הקורס

$config[zx-auto] not found$config[zx-overlay] not found