<%@ WebHandler Language="C#" Class="TrackClicksHandler" %> using System; using System.IO; using System.Text; using System.Web; using System.Web.Script.Serialization; using System.Text.RegularExpressions; using System.Collections.Generic; public class TrackClicksHandler : IHttpHandler { /* ======================================= CONFIG ======================================= */ private static readonly HashSet allowedGames = new HashSet(StringComparer.OrdinalIgnoreCase) { "snake", "muncher", "chain" }; private static readonly HashSet allowedZones = new HashSet(StringComparer.OrdinalIgnoreCase) { "HQ", "North", "South", "East", "West" }; private static readonly HashSet allowedDomains = new HashSet(StringComparer.OrdinalIgnoreCase) { "nipponindiaim.com" }; const int MAX_SCORE = 1000; const int MAX_TIME = 3600; /* ======================================= RATE LIMITING ======================================= */ private static readonly Dictionary> ipRequests = new Dictionary>(); private static readonly object rateLock = new object(); const int RATE_LIMIT = 20; const int RATE_WINDOW_SEC = 60; public void ProcessRequest(HttpContext context) { context.Response.ContentType = "application/json"; context.Response.ContentEncoding = Encoding.UTF8; try { SetSecurityHeaders(context); /* ======================================= RATE LIMIT ======================================= */ string ip = GetClientIp(context); if (IsRateLimited(ip)) { WriteError(context, 429, "Too many requests"); return; } /* ======================================= CONTENT-TYPE VALIDATION ======================================= */ if (context.Request.ContentType == null || !context.Request.ContentType.Contains("application/json")) { WriteError(context, 415, "Invalid content type"); return; } /* ======================================= READ BODY ======================================= */ string body; using (var reader = new StreamReader(context.Request.InputStream)) body = reader.ReadToEnd(); if (string.IsNullOrWhiteSpace(body)) { WriteError(context, 400, "Empty request body"); return; } var serializer = new JavaScriptSerializer(); Dictionary data; try { data = serializer.Deserialize>(body); } catch { WriteError(context, 400, "Invalid JSON"); return; } /* ======================================= EXTRACT + NORMALIZE INPUT ======================================= */ string email = GetStr(data, "email").Trim().ToLowerInvariant(); string zone = GetStr(data, "zone").Trim(); string game = GetStr(data, "game").Trim().ToLowerInvariant(); int score = GetInt(data, "score"); int timeSpent = GetInt(data, "timeSpent"); /* ======================================= STRICT VALIDATION (VAPT SAFE) ======================================= */ if (!allowedGames.Contains(game)) { WriteError(context, 400, "Invalid game"); return; } if (!allowedZones.Contains(zone)) { WriteError(context, 400, "Invalid zone"); return; } if (!IsValidCorporateEmail(email)) { WriteError(context, 400, "Invalid corporate email"); return; } if (score < 0 || score > MAX_SCORE) { WriteError(context, 400, "Invalid score"); return; } if (timeSpent < 0 || timeSpent > MAX_TIME) { WriteError(context, 400, "Invalid timeSpent"); return; } /* ======================================= SANITIZATION (AFTER VALIDATION) ======================================= */ email = Sanitize(email); zone = Sanitize(zone); game = Sanitize(game); /* ======================================= SAFE FILE WRITE ======================================= */ string folderPath = context.Server.MapPath("~/data/"); if (!Directory.Exists(folderPath)) Directory.CreateDirectory(folderPath); string filePath = Path.Combine(folderPath, "invest_clicks.csv"); bool writeHeader = !File.Exists(filePath); lock (typeof(TrackClicksHandler)) { using (FileStream fs = new FileStream(filePath, FileMode.Append, FileAccess.Write, FileShare.None)) using (StreamWriter writer = new StreamWriter(fs)) { if (writeHeader) { writer.WriteLine("Date,Email,Zone,Game,Score,TimeSpent,IP"); } writer.WriteLine(string.Format( "{0},{1},{2},{3},{4},{5},{6}", DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss"), EscapeCsv(email), EscapeCsv(zone), EscapeCsv(game), score, timeSpent, EscapeCsv(ip) )); } } /* ======================================= SUCCESS ======================================= */ context.Response.Write("{\"success\":true}"); } catch (Exception ex) { WriteError(context, 500, ex.Message); } } /* ======================================= EMAIL VALIDATION (STRICT) ======================================= */ private static bool IsValidCorporateEmail(string email) { if (string.IsNullOrWhiteSpace(email)) return false; if (email.Length > 120) return false; try { var addr = new System.Net.Mail.MailAddress(email); if (addr.Address != email) return false; return allowedDomains.Contains(addr.Host); } catch { return false; } } /* ======================================= RATE LIMIT ======================================= */ private bool IsRateLimited(string ip) { var now = DateTime.UtcNow; lock (rateLock) { if (!ipRequests.ContainsKey(ip)) ipRequests[ip] = new List(); var list = ipRequests[ip]; list.RemoveAll(t => (now - t).TotalSeconds > RATE_WINDOW_SEC); if (list.Count >= RATE_LIMIT) return true; list.Add(now); return false; } } private string GetClientIp(HttpContext context) { string ip = context.Request.Headers["X-Forwarded-For"]; if (!string.IsNullOrEmpty(ip)) return ip.Split(',')[0].Trim(); return context.Request.UserHostAddress ?? "unknown"; } /* ======================================= HELPERS ======================================= */ private void SetSecurityHeaders(HttpContext ctx) { ctx.Response.Headers["X-Content-Type-Options"] = "nosniff"; ctx.Response.Headers["X-Frame-Options"] = "DENY"; } private static string GetStr(Dictionary d, string key) { return d.ContainsKey(key) ? (d[key] ?? "").ToString() : ""; } private static int GetInt(Dictionary d, string key) { if (!d.ContainsKey(key)) return 0; int val; return int.TryParse(d[key].ToString(), out val) ? val : 0; } private static string Sanitize(string v) { if (string.IsNullOrEmpty(v)) return ""; v = Regex.Replace(v, @"[\x00-\x1F\x7F]", ""); if (Regex.IsMatch(v, @"^[=\+\-\@]")) v = "'" + v; if (v.Length > 120) v = v.Substring(0, 120); return v; } private static string EscapeCsv(string input) { if (input.Contains(",") || input.Contains("\"")) { input = input.Replace("\"", "\"\""); return "\"" + input + "\""; } return input; } private static void WriteError(HttpContext context, int code, string message) { context.Response.StatusCode = code; context.Response.Write("{\"success\":false,\"error\":\"" + message + "\"}"); } public bool IsReusable { get { return false; } } }