פתרון פשוט למניעת ספאם באתרים

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

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

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

איך זה עובד:

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

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

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

חסרונות השיטה

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

הטמעה באתר

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

קובץ ה־.htaccess בתיקייה בה נמצאים הקבצים הבעיתיים יראה כך:
RewriteEngine on
RewriteCond %{HTTP_COOKIE} !nospam_token=welcome
RewriteRule posting.php|login.php|profile.php /spam_jail/index.html

הוראות אלו יגרמו לכל מי שמנסה לגשת לדפים posting.php, login.php או profile.php (שלושת הקבצים הבעיתיים ביותר במערכות פורומים מסוג phpbb) לקבל דף HTML מיוחד במקום הדף שהם ביקשו, כאשר אין להם עוגייה nospam_token עם הערך "welcome". תרגישו חופשיים לשנות את שם העוגיה (האסימון) ואת הערך (הסיסמה) שלה.

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

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

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

טעינת העוגייה. הקוד יעלה את העמוד מחדש כאשר העוגייה קיימת בדפדפן, וינסה להטעין אותה שוב אם לא הצליח בפעם הראשונה. במידה והמשתמש מתבקש לאשר את טעינת העוגייה והוא עונה בסירוב, הוא יתבקש להטעין את העוגיה שוב.
<script language="JavaScript">
function main()
{
createCookie ("nospam_token", "welcome", 30);
if (readCookie("nospam_token")=="welcome")
window.setTimeout ("refresh (true);", 100);
else
window.setTimeout ("main();", 250);
}
</script>

טיפול בעוגיות –
<script language="JavaScript">
function createCookie(name,value,days) {
if (days) {
var date = new Date();
date.setTime(date.getTime()+(days*24*60*60*1000));
var expires = "; expires="+date.toGMTString();
}
else var expires = "";
document.cookie = name+"="+value+expires+"; path=/";
}
function readCookie(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for(var i=0;i < ca.length;i++) {
var c = ca[i];
while (c.charAt(0)==' ') c = c.substring(1,c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.l
ength);
}
return null;
}

טעינת העמוד מחדש. מתברר שזה דבר מסובך, ויש צורך בשלוש פונקציות שונות כדי לתמוך בכל הגירסאות והדפדפנים.
<script language="JavaScript">
function refresh()
{
var sURL = unescape(window.location.pathname);
window.location.href = sURL;
}
</script>
<script language="JavaScript1.1">
function refresh()
{
var sURL = unescape(window.location.pathname);
window.location.replace( sURL );
}
</script>
<script language="JavaScript1.2">
function refresh()
{
window.location.reload( false );
}
</script>

קריאה לקוד מדף ה־HTML –
<body onload="main()">

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

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

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

עוד באותו נושא

16 תגובות בנושא “פתרון פשוט למניעת ספאם באתרים”

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

    אלעד/תום/חנית – רוצים לעשות ניסוי קטן? 😉

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

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

  3. שלום
    לא עובד לי העניין מישהוא יכול הגיד לי למה
    כך זה כתוב
    RewriteCond %{HTTP_COOKIE} !nospam_token=welcome
    RewriteRule index.php /token/index.html

    העניין הוא שאם פונים אל הקובץ בנתיב מלא
    http://www.mywebsite.co.il/test/index.php
    הוא מפנה לקובץ /token/index.html לצורך יצירת העוגיה

    כשפונים לדף בכתובת
    http://www.mywebsite.co.il/test/
    כל העניין לא עובד

    אשמח לתגובה

    1. יכול להיות שקיימת הגדרה נוספת בדף? נסה לשים [L] בסוף השורה שמתייחסת לחוק הנ"ל, כדי לוודא שלא תועבר למקום אחר. אם בוצעה הפנייה לדף השער זה לא יעבוד כי טעינה מחודשת של הדף תטען מחדש את דף השער ולא את דף היעד.

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

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

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

                1. במקום לחסום תיקייה שלמה, אפשר לחסום רק את הקובץ הראשי של התיקייה. משהו כזה:

                  RewriteCond %{HTTP_COOKIE} !nospam_token=welcome
                  RewriteRule (^$|index.html|index.php) /token/index.html

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

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

השאר תגובה