Розв’язуємо безпекові проблеми у легасі-коді

💡 Усі статті, обговорення, новини про .NET — в одному місці. Приєднуйтесь до .NET спільноти!

Вітаю, шановний читачу! Маю понад 30 років досвіду в ІТ на різних посадах — від техліда до архітектора. Долучався до розробки складних програмних рішень у багатьох відомих українських компаніях, зокрема в Infopulse, Ciklum, 3Shape, Luxoft. Мій стек охоплює як бекенд, так і фронтенд. Маю досвід роботи з такими мовами програмування, як C/C++, Delphi, VB, Java, Perl, Python, а також із різноманітними JavaScript-фреймворками.

Останніми роками я працюю в Itera Ukraine, де спеціалізуюся на .NET та React. Основна моя діяльність — реінжиніринг легасі-застосунків, архітектурне проєктування та підвищення рівня їхньої безпеки.

У цій статті я поділюся ключовими проблемами безпеки, з якими ми зіткнулися під час міграції великого легасі-застосунка на мікросервісну архітектуру, а також ефективними способами їх усунення. Застосунок був створений понад 10 років тому і свого часу вже проходив міграцію — з десктоп-програми на веб. Кодову базу писало багато розробників різних національностей із використанням різних бібліотек та фреймворків.

Технологічний стек:

  • Бекенд: .NET (C#), частково VB; деякі частини виділені в окремі сервіси на .NET REST API, Python і Node.js.
  • Фронтенд: MVC + AngularJS (старі частини), Vue.js (нові частини).

Опис проблеми

У час створення застосунка питання безпеки не були серед ключових пріоритетів. Хоч певну увагу їм і приділяли, однак критичними вони не вважалися. Враховуючи, що сам застосунок пов’язаний з енергетичним сектором, після початку війни та зростання кібератак безпека стала питанням першої важливості.

Найбільша технічна конфа ТУТ!👇

Аудит, проведений на проєкті, виявив значні проблеми. Їх виокремили в одне велике завдання, яке доручили команді з трьох розробників — серед них був і я. Робота тривала пів року, адже сам проєкт доволі масштабний. Ми з колегами не були вузькими фахівцями з кібербезпеки, тож здобували необхідні знання безпосередньо в процесі. Цим практичним досвідом і хочу поділитися.

Передусім раджу ознайомитися з проєктами OWASP. Також є багато інструментів для автоматичного сканування вразливостей.

Інструменти безпеки

Першим нашим кроком було використати сканер Snyk. Він має безкоштовну версію з обмеженою функціональністю, проте рекомендую замовникам інвестувати в бізнес-ліцензію — це значно розширить можливості.

Встановити та запустити його в папці з проєктом дуже легко:

npm install -g snyk
snyk test -json | snyk-to-html > snyk-report.html

У великих організаціях процеси оновлення зазвичай ідуть повільно, ніхто не поспішає апгрейдитися на останні версії бібліотек. Через це проблеми з безпекою виникають часто.

✗ High severity vulnerability found in [email protected]
 - Info: https://snyk.io/vuln/SNYK-DOTNET-NEWTONSOFTJSON-123456
 - Introduced through: [email protected]
 - Змінено в: 12.0.1
 - Recommendation: Upgrade to [email protected]

Річ у тім, що пакет тягне за собою інші залежності. Часто доводиться змінювати навіть таку функціональність, яку складно або й неможливо адекватно протестувати. Тому оновлення залежностей до безпечніших версій — це лише початок танців з бубнами. У нашому випадку тільки на апгрейд витратили приблизно людино-тиждень.

Варто зазначити, що Snyk здатен виявляти вразливості й у JavaScript-коді. Ми відразу інтегрували Snyk у CI/CD-пайплайн:

trigger:

..
steps:
- task: UseNode@1
  inputs:
    version: '16.x'  # Актуальна версія Node.js для Snyk

- script: |
    npm install -g snyk
    snyk auth $(SNYK_TOKEN)
  displayName: 'Install and Authenticate Snyk'

- script: snyk test --all-projects --severity-threshold=high
  displayName: 'Run Snyk Security Scan'

- script: snyk monitor --all-projects
  displayName: 'Monitor Dependencies for Future Issues'

Наступним кроком ми підключили SonarQube для аналізу сорс-коду і також інтегрували його в CI/CD:

..
- task: SonarQubePrepare@5
  inputs:
    SonarQube: 'SonarQubeServiceConnection' # Set up in Azure DevOps
    scannerMode: 'MSBuild'
    projectKey: 'MyCSharpApp'

- script: |
    dotnet restore
    dotnet build --configuration Release
  displayName: 'Build Project'

- task: SonarQubeAnalyze@5
  displayName: 'Run SonarQube Analysis'

- task: SonarQubePublish@5
  displayName: 'Publish SonarQube Results'

Отриманий звіт містив сотню сторінок — проблеми варіювалися від банальних до складних. Ось приклади поширених помилок, які дуже часто трапляються в легасі-застосунку:

  • SQL-ін’єкції.
  • XSS (міжсайтовий скриптинг).
  • Проблеми авторизації та автентифікації.
  • Небезпечна робота з файлами.
  • Використання застарілих і небезпечних сторонніх бібліотек.
  • Відсутність суворої політики логування та моніторингу.
  • Небезпечна десеріалізація.
  • Помилки контролю доступу.
  • Прототипне забруднення (Prototype Pollution).
  • Витік інформації через заголовки, повідомлення про помилки або в коді.

Далі детальніше розглянемо характерні помилки, на які часто не звертають уваги.

1. SQL-ін’єкції

Проблема:

string query = "SELECT * FROM RiskMatrix WHERE RiskID = " + riskId +  “;”;
SqlCommand cmd = New SqlCommand(query, connection);
SqlDataReader reader = cmd.ExecuteReader();

Вразливість полягає в тому, що riskId може містити шкідливий код, наприклад 1=1, який поверне всі записи.

Або якщо замість riskId = 5 зловмисник введе рядок: 5; DROP TABLE RiskMatrix, тоді SQL-запит виглядатиме так:

SELECT * FROM RiskMatrix WHERE RiskID = 5; DROP TABLE RiskMatrix

Цей запит не тільки отримає дані, але й видалить таблицю RiskMatrix.

Виправлення:

string query = "SELECT * FROM RiskMatrix WHERE RiskID = @riskId";
SqlCommand cmd = new SqlCommand(query, connection);
cmd.Parameters.AddWithValue("@riskId", riskId);
SqlDataReader reader = cmd.ExecuteReader();

Параметризація запитів — найкращий спосіб уникнути SQL-ін’єкцій. riskId більше не є частиною SQL-команди, а передається окремо як параметр.

2. XSS (міжсайтовий скриптинг)

Проблема:

return "

<div>" + userComment + "</div>

";

Цей код відображає дані з бази без жодної перевірки або обробки. У проєкті було багато місць, де користувач міг залишити опис або коментар.

Виправлення:

using System.Web;
string safeOutput = HttpUtility.HtmlEncode(userComment);
return "

<div>" + safeOutput + "</div>

";

Використання HtmlEncode дає змогу перекодовувати спеціальні символи HTML у безпечні еквіваленти. Це гарантує, що дані, введені користувачем, не будуть сприйняті браузером як HTML або JavaScript.

Крім того, для захисту від XSS-атак ми впровадили Content Security Policy (CSP), яка забороняє виконання небезпечних скриптів. CSP можна задати як через HTTP-заголовки, так і за допомогою метатегів у HTML.

Приклад конфігурації CSP у web.config для IIS:

<configuration>
    <system.webServer>
        

<httpProtocol>
            <customHeaders>
                <add name="Content-Security-Policy" value="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self'; img-src 'self' data:; object-src 'none'" />

            </customHeaders>
        </httpProtocol>
    </system.webServer>
</configuration>

ASP.NET MVC (до .NET Core)

В ASP.NET MVC 5 і нижче можна встановити заголовок в ActionFilter або методі Application_BeginRequest в Global.asax.cs:

Application_BeginRequest в Global.asax.cs:
protected void Application_BeginRequest(object sender, EventArgs e)
{
    HttpContext.Current.Response.Headers.Add("Content-Security-Policy", 
        "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self'; img-src 'self' data:; object-src 'none'");
}

Або у фільтрі MVC:

public class CspFilter : ActionFilterAttribute
{
    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        filterContext.HttpContext.Response.Headers.Add("Content-Security-Policy", 
            "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self'; img-src 'self' data:; object-src 'none'");
        base.OnResultExecuting(filterContext);
    }
}

Потім додаємо його до Controller:

[CspFilter]
public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View();
    }
}

Відразу зазначу, що в .NET Core це робиться інакше:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.Use(async (context, next) =>
    {
        context.Response.Headers.Add("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self'; img-src 'self' data:; object-src 'none'");
        await next();
    });

    app.UseRouting();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

3. Проблеми авторизації та аутентифікації

У нашому проєкті був уразливий механізм обробки токенів доступу, який можна було підробити. Основна проблема полягала у відсутності перевірки підпису токена аутентифікації. Якщо система використовує JWT або інші токени для ідентифікації користувачів, їхній підпис обов’язково має перевірятися перед обробкою запиту. В іншому разі зловмисник може підробити токен і видати себе за іншого користувача.

Проблема:

var token = Request.Headers["AuthToken"];
var userId = DecryptToken(token); // Немає перевірки підпису!
var user = GetUserById(userId);

У цьому випадку будь-який токен, навіть підроблений, проходить обробку, що дозволяє отримати доступ до системи.

Виправлення:

var token = Request.Headers["AuthToken"];
var validatedToken = ValidateAndDecodeToken(token);
if (validatedToken == null) {
    return Unauthorized();
}
var user = GetUserById(validatedToken.UserId);

Тепер система перевіряє підпис токена перед його розшифруванням, що унеможливлює підробку даних користувача.

Ще одна проблема полягала в тому, що код частково дозволяв отримати доступ до чужих даних без належної перевірки прав доступу:

public IActionResult GetAssets(int departmentId)
{
    var user = _dbContext.Assets.Find(departmentId);
    return Ok(user);
}

4. Небезпечна робота з файлами

Проблема: у багатьох частинах застосунку користувачам дозволяли завантажувати файли без перевірки їхнього типу та вмісту. Це створювало потенційну загрозу, оскільки хакер міг, наприклад, перейменувати виконуваний файл .exe на .txt і завантажити його на сервер. Без перевірки вмісту та розширення файлу його можна було б використати для подальших атак.

Небезпечний код:

public void ProcessRequest(HttpContext context)
{
    HttpPostedFile file = context.Request.Files[0];
    file.SaveAs(context.Server.MapPath("~/uploads/" + file.FileName));
    context.Response.Write("File has been successfully uploaded.");
}

Цей код приймає будь-який файл без перевірки, що може зробити сервер сховищем потенційно шкідливих файлів.

Складність полягала ще й у тому, що таких місць завантаження файлів було понад два десятки. Код не завжди був створений через копіювання, тож навіть за наявності повторів це ускладнювало виправлення. У багатьох випадках узагалі не перевірялося, чи відповідає файл очікуваному формату або чи він принаймні валідний. Тому це потребувало універсального рішення без значного рефакторингу.

Виправлення: щоб вирішити проблему, ми реалізували спеціальний HTTP-обробник на рівні IIS, який перевіряв не лише розширення файлу, а і його фактичний вміст.

Конфігурація IIS

<system.webServer>
    

<handlers>
        <add name="FileUploadHandler" path="uploads/*" verb="POST" type="FileUploadHandler, MyApp" resourceType="Unspecified" />

    </handlers>
</system.webServer>

JSON-мапінг дозволених файлів

Оскільки різні частини системи дозволяли завантаження різних типів файлів, ми централізували перевірку через окремий JSON-файл, який містив дозволені розширення для кожного ендпоїнту. Код внизу — схематичний:

{
    "mappings": [
        {
            "path": "user/reports/*",
            "allowedExtensions": [".xls", ".xlsx", ".csv", ".pdf"]
            “maxSize” : “40 000”
        },
        {
            "path": "maps/images/*",
            "allowedExtensions": [".jpg", ".png", ".gif"]
        },
        {
            "path": "contracts/documents/*",
            "allowedExtensions": [".doc", ".docx", ".txt", ".pdf"]
        }
    ]
}

Реалізація HTTP-обробника

public class FileUploadHandler : IHttpHandler
{
    private readonly Dictionary<string, List<string>> _allowedFileTypes;
    
    public FileUploadHandler()
    {
        _allowedFileTypes = LoadFileMappings();
    }
    
    public void ProcessRequest(HttpContext context)
    {
        HttpPostedFile file = context.Request.Files[0];
        string requestPath = context.Request.Path;

        if (!IsValidFile(file, requestPath))
        {
            context.Response.StatusCode = 400;
            context.Response.Write("Invalid file or format !");
            return;
        }
        
        file.SaveAs(context.Server.MapPath("~/uploads/" + Path.GetFileName(file.FileName)));
        context.Response.Write("File has been uploaded.");
    }

private bool IsValidFile(HttpPostedFile file, string requestPath)
    {
        string extension = Path.GetExtension(file.FileName).ToLower();
        
        if (!_allowedFileTypes.Any(mapping => requestPath.StartsWith(mapping.Key) && mapping.Value.Contains(extension)))
        {
            return false;
        }
        
        return ValidateFileContent(file, extension);
    }

    private bool ValidateFileContent(HttpPostedFile file, string extension)
    {
        try
        {
            byte[] fileSignature = new byte[4];
            file.InputStream.Read(fileSignature, 0, 4);
            
            if (IsExecutableFile(fileSignature))
            {
                return false;
            }

            switch (extension)
            {
                case ".json":
                    using (var reader = new StreamReader(file.InputStream))
                    {
                        string content = reader.ReadToEnd();
                        JObject.Parse(content); // Перевірка на валідність JSON
                    }
                    break;
                case ".pdf":
                    if (!fileSignature.SequenceEqual(new byte[] { 0x25, 0x50, 0x44, 0x46 })) // PDF signature
                        return false;
                    break;
                 // lots of other case..
            }
        }
        catch (Exception)
        {
            return false;
        }

        return true;
    }

    private bool IsExecutableFile(byte[] signature)
    {
        // Перевірка на виконувані файли (EXE, DLL)
        byte[][] executableSignatures = new byte[][]
        {
            new byte[] { 0x4D, 0x5A } // EXE/DLL (MZ Header)
        };
        return executableSignatures.Any(sig => signature.Take(sig.Length).SequenceEqual(sig));
    }
    
    private Dictionary<string, List<string>> LoadFileMappings()
    {
        string configPath = HttpContext.Current.Server.MapPath("~/App_Data/FileMappings.json");
        string json = File.ReadAllText(configPath);
        var mappings = JObject.Parse(json)["mappings"].ToObject<List<FileMapping>>();
        return mappings.ToDictionary(m => m.Path, m => m.AllowedExtensions);
    }
    
    public bool IsReusable => false;
}

public class FileMapping
{
    public string Path { get; set; }
    public List<string> AllowedExtensions { get; set; }
}

Що покращила ця реалізація

Гнучкість: адміністратори можуть змінювати дозволені типи файлів без редагування коду.
Безпека: перевіряється не лише розширення файлів, а і їхній фактичний вміст.
Зручність підтримки: одна точка керування всіма дозволеними форматами для різних ендпоїнтів.

Було також реалізовано додаткові засоби захисту:

  • обмеження на максимальний розмір файлу;
  • вимкнення виконання файлів у каталозі завантажень;
  • використання антивірусного сканування перед збереженням файлів.

Завдяки цим заходам нам вдалося запобігти завантаженню шкідливих файлів і підвищити стабільність системи.

5. Витік інформації через заголовки та повідомлення про помилки

Проблема: деталізовані повідомлення про помилки можуть розкрити критичну інформацію, таку як внутрішня структура сервера, шляхи до файлів або навіть SQL-запити. З одного боку, це зручно для розробника, але водночас може стати джерелом цінної інформації для зловмисника.

try
{
    var data = File.ReadAllText("config.json");
}
catch (Exception ex)
{
    return BadRequest(ex.ToString());
}

У випадку помилки користувач отримає повний стек-трейс, що може допомогти хакеру зрозуміти структуру системи та знайти нові вразливості.

Виправлення:

try
{
    var data = File.ReadAllText("config.json");
}
catch (Exception ex)
{
    _logger.LogError($"Error while reading config: {ex.Message}");
    return BadRequest("Error while processing the request. Please contact support.");
}

Деталі помилок тепер логуються на сервері, але не відображаються користувачеві. Це унеможливлює витік критичної інформації.

Окрім того, ми відключили розголошення інформації через заголовки в ASP.NET Core:

app.Use(async (context, next) =>
{
    context.Response.Headers.Remove("X-Powered-By");
    context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
    context.Response.Headers.Add("X-Frame-Options", "DENY");
    await next();
});

Це запобігає атакам Clickjacking, а також зменшує ризик витоку інформації про сервер.

6. Небезпечна десеріалізація

Проблема: небезпечна десеріалізація виникає, коли програма без перевірки завантажує та розшифровує об’єкти зі сторонніх джерел. У цьому випадку BinaryFormatter під час десеріалізації може виконати довільний код — це дозволяє зловмиснику впровадити шкідливі об’єкти, які можуть отримати доступ до файлів, виконати команди або змінити дані в системі.

BinaryFormatter formatter = new BinaryFormatter();
using (FileStream stream = new FileStream("userData.dat", FileMode.Open))
{
    var user = (User)formatter.Deserialize(stream);
}

Виправлення: замість BinaryFormatter ми використали JsonConvert, але обмежуємо можливість десеріалізувати довільні типи, встановивши TypeNameHandling = TypeNameHandling.None. Це запобігає виконанню довільного коду та обмежує десеріалізацію лише очікуваними структурами даних, що значно підвищує рівень безпеки.

var settings = new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.None,
    Formatting = Formatting.Indented
};
var user = JsonConvert.DeserializeObject<User>(jsonString, settings);

7. Витік секретних ключів у коді

Проблема: розробники часто залишають у коді секретні ключі API, токени доступу або паролі, особливо під час тестування. Якщо такі дані потраплять у репозиторій (наприклад, GitHub), їх можуть знайти зловмисники, навіть коли сам репозиторій закритий. Наприклад:

public static string ApiKeyDebug = "00000000-1111-2222-3333-444444444444";
var connectionString = "Server=myserver;Database=mydb;User Id=admin;Password=admin;";

Такі дані можуть випадково потрапити в загальнодоступні репозиторії або журнали помилок, що відкриє зловмисникам прямий доступ до критично важливих систем.

Виправлення:

1. Виносьте секрети в змінні середовища або захищені сховища

Замість того щоб зберігати ключі в коді, використовуйте змінні середовища або Azure Key Vault чи AWS Secrets Manager:

var apiKey = Environment.GetEnvironmentVariable("MY_APP_API_KEY");

2. Використовуйте файли конфігурації, але не зберігайте їх у Git

У appsettings.json можна вказати placeholder, а реальні значення зберігати в appsettings.Development.json, який не завантажується в репозиторій.

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=myserver;Database=mydb;User Id=${DB_USER};Password=${DB_PASSWORD}"
  }
}

8. Прототипне забруднення в AngularJS

Ще одна проблема, з якою ми зіткнулися в JavaScript-середовищі, — це прототипне забруднення (Prototype Polution). Ця вразливість дає зловмиснику змогу змінювати глобальні прототипи (Object.prototype), що може призвести до серйозних наслідків: неправильна логіка авторизації, виконання шкідливого коду та навіть відмова в обслуговуванні (DoS-атаки).

Проблема:

Уразливий бекенд API

У контексті AngularJS ця атака може впливати на об’єкти, що передаються у двостороннє зв’язування даних ($scope) або приходять через API.

[HttpPost]
public IActionResult UpdateUser([FromBody] User user)
{
    _dbContext.Users.Update(user);
    _dbContext.SaveChanges();
    return Ok();
}

Якщо зловмисник надішле спеціально створений об’єкт,

{
  "__proto__": {
    "isAdmin": true
  }
}

він змінить глобальний Object.prototype, і кожен об’єкт у системі отримає isAdmin: true, навіть якщо в нього немає такого поля.

Уразливий фронтенд в AngularJS

Уявімо, що AngularJS-код працює з цим API та отримує дані з JSON.parse() без перевірки:

$http.post('/api/update-user', userInput)
    .then(response => {
        $scope.user = response.data; 
    });

Атакуючий передає в API:

{
  "__proto__": {
    "isAdmin": true
  }
}

Це змінює глобальний об’єкт Object.prototype:

console.log({}.isAdmin); // true!

Тепер будь-який код, який перевіряє if (user.isAdmin), отримає true, навіть якщо користувач насправді не має таких прав!

Виправлення:

1. Використовуйте Object.create(null) для захисту.

let userData = Object.create(null);

Це гарантує, що у вашого об’єкта не буде __proto__, і його неможливо змінити глобально.

2. Перевіряйте вхідні дані перед обробкою

Перед тим як передати введені користувачем дані у фронтенд або бекенд, видаляйте небезпечні властивості.

На бекенді (.NET)

public static bool IsValidJson(string json)
{
    if (json.Contains("__proto__") || json.Contains("constructor"))
    {
        return false;
    }
    return true;
}

На фронтенді (AngularJS)

function sanitizeInput(input) {
    if (typeof input === 'object' && input !== null) {
        if ('__proto__' in input || 'constructor' in input) {
            throw new Error("Prototype Pollution detected!");
        }
        Object.keys(input).forEach(key => sanitizeInput(input[key]));
    }
}

3. Використовуйте бібліотеки для очищення даних

Щоб автоматично видаляти шкідливі властивості, можна використати DOMPurify або lodash.cloneDeep():

З lodash

import _ from 'lodash';
let safeData = _.cloneDeep(userInput);

З DOMPurify

import DOMPurify from 'dompurify';
let sanitizedData = DOMPurify.sanitize(userInput);

4. Використовуйте hasOwnProperty() для перевірки властивостей

Коли перевіряєте, чи є поле в об’єкті, замість if (obj.property) використовуйте hasOwnPropert(y):

Небезпечно:

if (user.isAdmin) {  // isAdmin може бути в Object.prototype!
    grantAccess();
}

Безпечно:

if (user.hasOwnProperty('isAdmin') && user.isAdmin) {
    grantAccess();
}

Тестування API за допомогою OWASP ZAP

Вразливості, такі як SQL-ін’єкції, XSS, небезпечні заголовки тощо, можна виявити за допомогою OWASP ZAP (Zed Attack Proxy) — потужного інструмента для автоматизованого тестування API.

Виконуйте сканування за допомогою команди:

zap-cli quick-scan -r https://your-api-endpoint.com

Або використовуйте Docker:

docker run -u zap -p 8080:8080 -i owasp/zap2docker-stable zap.sh -daemon -host 0.0.0.0 -port 8080

Висновки

Питанню безпеки, так само як і якості коду, потрібно приділяти належну увагу із самого початку розробки. Інакше проблеми накопичуються, як снігова куля, а їхнє вирішення згодом потребує значних зусиль. Якщо у вас є подібний легасі-застосунок, раджу провести аудит безпеки та впровадити DevSecOps-підхід у розробку.

Подяка

Подяка моєму колезі Кирилові Велігоцькому за ревью статті перед публікацією.
👍ПодобаєтьсяСподобалось33
До обраногоВ обраному15
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Коментар порушує правила спільноти і видалений модераторами.

Дуже корисна добірка вразливостей з їх описом та дієвою рецептурою запобігання. Щира дяка!

Також подиви на цей код:
var result = dbContext.RiskMatrix.Where(r => r.RiskID == riskId).ToList();
У цьому коді Entity Framework і LINQ автоматично параметризують value, і це також безпечно.
Таким чином нічого не зламається, навіть якщо користувач передасть «1=1» або інший шкідливий SQL-код.

Так, ви абсолютно праві — у випадку використання LINQ з Entity Framework, значення автоматично параметризуються, і це захищає від SQL-ін’єкцій.
Мій приклад стосувався саме випадків, коли розробники вручну формують SQL-запити як рядки (а таке, на жаль, досі часто трапляється в легасі-коді).
Так, якщо ви напишите

   var result = dbContext.RiskMatrix.Where(r => r.RiskID == riskId).ToList();
Entity Framework (EF) і LINQ автоматично створюють параметризовані SQL-запити, тобто riskId буде передано в запит не як частина SQL-рядка, а як SQL-параметр (@p0, @p1, тощо).
Це дiйсно захищає від SQL-ін’єкцій, навіть якщо хтось спробує передати «1=1» як riskId.
У цьому випаку EF згенерує щось на кшталт
  SELECT * FROM RiskMatrix WHERE RiskID = @p0
І значення @p0 = «1=1» викличе помилку перетворення типу, а не виконає шкідливий код.
Що слід додатково врахувати
1. Це стосується саме Entity Framework. Якщо хтось напише:
string query = "SELECT * FROM RiskMatrix WHERE RiskID = " + riskId;
var result = dbContext.Database.SqlQuery<RiskMatrix>(query).ToList();
— тут вже жодної автоматичної параметризації немає, і цей код вразливий до SQL-ін’єкцій.

2. Якщо riskId є рядком, і навіть якщо EF «захистить», може бути викликана SQL-помилка або несподіваний результат, тому все одно краще валідувати типи на вхідному рівні.

3. Ручна ін’єкція SQL у EF через .FromSqlRaw() або .ExecuteSqlRaw() — це теж вразливе місце, якщо не передаються параметри явно:
Небезпечно:

   context.Users.FromSqlRaw($"SELECT * FROM Users WHERE Name = '{name}'");
Безпечно:
   context.Users.FromSqlRaw("SELECT * FROM Users WHERE Name = @p0", name);

То є добре. Дякую за роз’яснення. Поки такий легасі-код існує — у нас завжди буде робота :)

Про

SQL-ін’єкції

цікаве рішення. Я думав шо це вирішується автоматично на рівні Entity Framework щє до додавання в тіло SQL-запиту. На рівні самої entity яка приймає параметри від користувача :)

Підписатись на коментарі