💡 דוגמה מלאה עובדת זמינה ב‑GitHub:
load-untrusted-documents-safely-python

הדרך הישנה הייתה כואבת

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

with signature.Signature(upload_path) as sign:
    save_page_preview(sign, thumbnail_path)

מה שהשורות האלו עשו, לפני GroupDocs.Signature 26.9, היה לאסוף כל כתובת שהמסמך הפנה אליה. קובץ Word יכול להכיל תמונה שהוא לא מחזיק – הקובץ מאחסן URL, וכל מה שפותח אותו מוריד את ה‑URL. במחשב שולחני זהו תכונה. בשרת שמקבל העלאות, המשמעות היא שהאדם ששיגר לך את הקובץ מחליט לאילו כתובות התשתית שלך תבצע בקשות.

ההתקפה נקראת server‑side request forgery, ויש שלושה צורות שכדאי לציין. כתובת פנימית שאינה נגישה מהאינטרנט נגישה מהשרת שלך, ולכן מסמך מתוחכם יכול לגרום לשירות שלך לבקש http://169.254.169.254/ או נקודת קצה של מנהל על localhost. נתיב UNC יכול לגרום למארח Windows לאמת חיצונית, ולמסור אישורים לשרת שנשלט על‑ידי תוקף. וקישור למארח שלעולם לא משיב מחזיק את חוט הטעינה עד לפקיעת הזמן, מה שממלא את מאגר העובדים עם מסמכים שנראים חסרי נזק.

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

יש דרך טובה יותר

טעינה בטוחה של מסמכים היא ההתנהגות של GroupDocs.Signature עבור Python שמסרבת לבצע את הבקשות הללו. החל מגרסה 26.9, LoadOptions.skip_external_resources מוגדר כברירת מחדל ל‑True, ולכן שלוש השורות האלו כעת אינן מבצעות שום בקשה ומציגות מציין מקום במקום התמונה המקושרת.

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

הדרך החדשה: שלושה מצבי טעינה

שלב 1 – שמור על ברירת המחדל לכל דבר לא מהימן

בלי LoadOptions כלל:

with signature.Signature(source_path) as sign:
    return save_page_preview(sign, preview_path)

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

שלב 2 – רשימת לבן של מארח שבבעלותך בפועל

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

load_options = LoadOptions()
load_options.whitelisted_resources = [trusted_address]

with signature.Signature(source_path, load_options) as sign:
    return save_page_preview(sign, preview_path)

הכלל המתאים דורש תשומת לב. הוא בודק תת‑מחרוזת ללא תלות ברישיות על כתובת המשאב, מה שהופך קטע קצר למסוכן: github תואם ל‑github.attacker.example/payload.png בדיוק כמו המארח שהתכוונת אליו. השתמש בסכמה, במארח ובנתיב – דוגמה זו מוסיפה לרשימת הלבן את raw.githubusercontent.com/groupdocs-signature/.

שלב 3 – אפשר הכל, במודע

התנהגות לפני‑26.9, עדיין זמינה:

load_options = LoadOptions()
load_options.skip_external_resources = False

מתאים למסמכים שהיישום שלך יצר. מלכודת אחת: למאפיין הישן load_external_resources יש קוטביות הפוכה, ולכן skip_external_resources = False מחליף את load_external_resources = True. אם מעתיקים ערך מהמאפיין הישן, הופכים את מצב האבטחה ללא שגיאה שמודיעה על כך.

ציד‑צד: לפני מול אחרי

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

מצב טעינה גודל תצוגה מקדימה בקשות יוצאות
ברירת מחדל (26.9 והלאה) 16,435 בתים אף אחת
מארח ברשימת לבן 51,738 בתים אחת, לכתובת המורשית
כל המשאבים (ברירת מחדל לפני 26.9) 51,738 בתים אחת לכל משאב מקושר

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

מה נחשב למשאב חיצוני?

צומצם ממה שאנשים מצפים, ולכן השדרוג לרוב אינו מפתיע. תמונות מקושרות ולא מוטמעות, שדות INCLUDEPICTURE, תמונות מקושרות במצגות ובגיליונות אלקטרוניים, והדימויים וקבצי ה‑CSS ש‑SVG מתייחס אליהם. תוכן מוטמע נשאר ללא שינוי, מכיוון שהוא כבר בתוך הקובץ ואין צורך בבקשה כדי להציגו.

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

דוגמה מהעולם האמיתי: העלאה שנחתמת

המקרה שלשמו קיים שינוי ברירת המחדל. מסמך מגיע מבחוץ, ואתה צריך להוסיף לו חתימה:

with signature.Signature(source_path) as sign:
    options = QrCodeSignOptions("Approved by GroupDocs.Signature")
    options.encode_type = QrCodeTypes.QR
    options.left = 400
    options.top = 50
    options.width = 120
    options.height = 120

    result = sign.sign(output_path, options)

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

מה עוד משתנה כשמשדרגים?

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

חשוב לציין בנפרד: SVG. קובץ SVG יכול להתייחס לתמונות ו‑CSS דרך URL, והפניות אלו נחשבות למשאבים חיצוניים תחת אותו כלל, ו‑SVG הוא גם פורמט העלאה נפוץ וגם וקטור SSRF נפוץ. שירות שמקבל אווטרים ב‑SVG ומציגם בצד השרת הוא בדיוק הסוג של מערכת שהשינוי הזה מגן עליו.

פרט Python אחד: איך נכתבת התצוגה המקדימה

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

def create_page_stream(page_data):
    return open(preview_path, "wb")

def release_page_stream(page_data, page_stream):
    page_stream.close()

preview_options = PreviewOptions(create_page_stream, release_page_stream)
preview_options.preview_format = PreviewOptions.PreviewFormats.PNG
sign.generate_preview(preview_options)

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

סיכום

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

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

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

משאבים נוספים