SQL injection filter for WebApi

Keywords: C# SQL Database Attribute

Development tools: Visual Studio 2017
C × version: C × 7.1

The most effective way to prevent SQL injection is to use parameterized queries when invoking the database.
But if you are taking over an old WebApi project and don't want to change the code of many database access layers, how to do it.

My solution is to add a filter.

Write the filter method first, and then the code

using System;
using System.Collections.Generic;
using System.Web;

namespace Test
{
    /// <summary>
    ///Prevent SQL injection
    /// </summary>
    public class AntiSqlInject
    {
        public static AntiSqlInject Instance = new AntiSqlInject();

        /// <summary>
        ///Initialize filter method
        /// </summary>
        static AntiSqlInject()
        {
            SqlKeywordsArray.AddRange(SqlSeparatKeywords.Split('|'));
            SqlKeywordsArray.AddRange(Array.ConvertAll(SqlCommandKeywords.Split('|'), h => h + " "));
            SqlKeywordsArray.AddRange(Array.ConvertAll(SqlCommandKeywords.Split('|'), h => " " + h));
        }

        private const string SqlCommandKeywords = "and|exec|execute|insert|select|delete|update|count|chr|mid|master|" +
                                                  "char|declare|sitename|net user|xp_cmdshell|or|create|drop|table|from|grant|use|group_concat|column_name|" +
                                                  "information_schema.columns|table_schema|union|where|select|delete|update|orderhaving|having|by|count|*|truncate|like";

        private const string SqlSeparatKeywords = "'|;|--|\'|\"|/*|%|#";

        private static readonly List<string> SqlKeywordsArray = new List<string>();

        /// <summary>
        ///Is it safe
        /// </summary>
        ///< param name = "input" > input < / param >
        ///< returns > return < / returns >
        public bool IsSafetySql(string input)
        {
            if (string.IsNullOrWhiteSpace(input))
            {
                return true;
            }
            input = HttpUtility.UrlDecode(input).ToLower();

            foreach (var sqlKeyword in SqlKeywordsArray)
            {
                if (input.IndexOf(sqlKeyword, StringComparison.Ordinal) >= 0)
                {
                    return false;
                }
            }
            return true;
        }

        /// <summary>
        ///Return security string
        /// </summary>
        ///< param name = "input" > input < / param >
        ///< returns > return < / returns >
        public string GetSafetySql(string input)
        {
            if (string.IsNullOrEmpty(input))
            {
                return string.Empty;
            }
            if (IsSafetySql(input)) { return input; }
            input = HttpUtility.UrlDecode(input).ToLower();

            foreach (var sqlKeyword in SqlKeywordsArray)
            {
                if (input.IndexOf(sqlKeyword, StringComparison.Ordinal) >= 0)
                {
                    input = input.Replace(sqlKeyword, string.Empty);
                }
            }
            return input;
        }
    }
}

Then the filter, first the code

using System.Web.Http.Controllers;
using System.Web.Http.Filters;

namespace Test
{
    /// <inheritdoc>
    ///     <cref></cref>
    /// </inheritdoc>
    /// <summary>
    ///SQL injection filter
    /// </summary>
    public class AntiSqlInjectFilter : ActionFilterAttribute
    {
        /// <inheritdoc />
        /// <summary>
        /// </summary>
        /// <param name="filterContext"></param>
        public override void OnActionExecuting(HttpActionContext filterContext)
        {
            base.OnActionExecuting(filterContext);
            var actionParameters = filterContext.ActionDescriptor.GetParameters();

            var actionArguments = filterContext.ActionArguments;

            foreach (var p in actionParameters)
            {
                var value = filterContext.ActionArguments[p.ParameterName];

                var pType = p.ParameterType;

                if (value == null)
                {
                    continue;
                }
                //No filtering is required if it is not a value type or interface
                if (!pType.IsClass) continue;

                if (value is string)
                {
                    //Filter string type
                    filterContext.ActionArguments[p.ParameterName] = AntiSqlInject.Instance.GetSafetySql(value.ToString());
                }
                else
                {
                    //It is a class. It filters the attributes of string type in the class attribute
                    var properties = pType.GetProperties();
                    foreach (var pp in properties)
                    {
                        var temp = pp.GetValue(value);
                        if (temp == null)
                        {
                            continue;
                        }
                        pp.SetValue(value, temp is string ? AntiSqlInject.Instance.GetSafetySql(temp.ToString()) : temp);
                    }
                }
            }

        }
    }
}

The idea is to add a filter to inherit ActionFilterAttribute, override OnActionExecuting method, get the input parameter, and filter all data of string type in the input parameter. In two cases, one is that the parameter is of type string, and the other is the attribute of the class. The filter is ready.

There are two ways to use the filter. One is to add the filter to the specific method

        [HttpPut,Route("api/editSomething")]
        [AntiSqlInjectFilter]
        public async Task<bool> EditSomeThingAsync([FromBody]SomeThingmodel)
        {
            var response = await SomeThingBusiness.Editsync(model);
            return response;
        }

One is global configuration, which adds a filter to the Register method in the WebApiConfig.cs file

using System.Web.Http;

namespace Test
{
    /// <summary>
    ///WebApi configuration
    /// </summary>
    public static class WebApiConfig
    {
        /// <summary>
        ///Register configuration service
        /// </summary>
        /// <param name="config"></param>
        public static void Register(HttpConfiguration config)
        {                      
            // Web API routing
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
            //Global configuration prevents SQL injection filtering
            config.Filters.Add(new AntiSqlInjectFilter());
        }
    }
}

The test is valid.

Posted by njm on Sun, 09 Feb 2020 10:56:35 -0800