当参数不是字符串时,不参数化 SQL 查询是否安全?

Is it safe to not parameterize an SQL query when the parameter is not a string?

提问人:johnnyRose 提问时间:9/18/2015 最后编辑:johnnyRose 更新时间:8/28/2020 访问量:15053

问:

SQL注入方面,我完全理解参数化的必要性;这是书中最古老的技巧之一。但是什么时候可以证明参数化是合理的?是否有任何数据类型被认为是“安全的”,可以不进行参数化?stringSqlCommand

例如:我不认为自己接近 SQL 专家,但我想不出任何可能容易受到 SQL 注入影响的情况,接受 a 或 an 并将其直接连接到查询中。boolint

我的假设是否正确,或者这可能会在我的程序中留下巨大的安全漏洞?

为了澄清起见,这个问题被标记为 ,这是一种强类型语言;当我说“参数”时,可以考虑类似 .public int Query(int id)

C# SQL SQL注入 SQLCoilate 参数化查询

评论

15赞 Scott Chamberlain 9/18/2015
如果不使用参数,您将无法获得缓存查询计划的好处,它需要为您提供的每个新输入组合制定单独的查询计划。
6赞 johnnyRose 9/18/2015
@MatthewWhited 你怎么知道它花费的时间更少?这种情况在当前开发人员和以前开发人员的某些项目中随处可见。如果它真的提高了安全性,请发布答案。为了澄清起见,我确实同意最好参数化,显然。但这并不是我的问题。
7赞 Salman A 9/18/2015
参数化查询主要用于性能和优化。防止 SQL 注入是一种副作用。
14赞 Sir Swears-a-lot 9/19/2015
我认为 OP 提出了一个有效的问题。他试图评估解决潜在风险的成本/收益。这个等式会随着潜在风险的变化而变化。如果风险为零,我也不会这样做。他被问到一个关于潜力的技术问题,而不是主观判断你是否认为它值得他花时间。OP 是唯一可以拨打该电话的人。
10赞 Sir Swears-a-lot 9/19/2015
解释一下自己:我是dba。我欣赏并尊重最佳实践,在一个完美的世界里,所有的代码都是完美的。可悲的是,在我工作的世界里,我需要解决的问题比我有时间解决它们还要多。这意味着要确定优先次序。IMO 重写已经有效、安全且性能达到可接受水平的代码听起来像是一种奢侈。(我买不起)

答:

23赞 Reza Aghaei 9/18/2015 #1
"SELECT * FROM Table1 WHERE Id=" + intVariable.ToString()

安全

没关系,
攻击者不能在你键入的 int 变量中注入任何内容。

性能

不行。

最好使用参数,这样查询将编译一次并缓存以供下次使用。下次即使使用不同的参数值,查询也会被缓存,不需要在数据库服务器中编译。

编码风格

糟糕的做法。

  • 参数更具可读性
  • 也许它让你习惯了没有参数的查询,然后也许你犯了一次错误,以这种方式使用字符串值,然后你可能应该告别你的数据。坏习惯!

"SELECT * FROM Product WHERE Id=" + TextBox1.Text

虽然这不是你的问题,但也许对未来的读者有用:

安全

灾难!

即使字段是整数,您的查询也可能受到 SQL 注入的影响。假设您的应用程序中有一个查询。攻击者可以插入文本框,查询将是:Id"SELECT * FROM Table1 WHERE Id=" + TextBox1.Text1; DELETE Table1

SELECT * FROM Table1 WHERE Id=1; DELETE Table1

如果不想在此处使用参数化查询,则应使用类型化值:

string.Format("SELECT * FROM Table1 WHERE Id={0}", int.Parse(TextBox1.Text))

您的问题

我的问题之所以出现,是因为一位同事写了一堆查询 连接整数值,我想知道它是否是一个 浪费我的时间来检查和修复所有这些问题。

我认为更改这些代码不是浪费时间。确实建议改变!

如果您的同事使用 int 变量,则没有安全风险,但我认为更改这些代码不会浪费时间,确实建议更改这些代码。它使代码更具可读性、更易于维护,并使执行速度更快。

评论

0赞 Joel Coehoorn 9/20/2018
即使是第一种选择也不能完全保证安全。的行为由操作系统配置项目确定,该配置项目易于更改以包含任意文本。.ToString()
0赞 ToolmakerSteve 8/28/2020
@JoelCoehoorn - 如果攻击者可以访问操作系统配置,他是否已经渗透到您的安全中?[尽管最好的安全性是冗余安全性,所以我同意没有理由以具有这种风险的方式进行编码]
1赞 Joel Coehoorn 8/28/2020
@ToolmakerSteve 对于某些应用程序,这可能由最终用户的客户端计算机决定。
0赞 ToolmakerSteve 8/28/2020
@JoelCoehoorn - 极好的观点。即使数据库值本身应始终是固定区域性;仅向用户显示将使用区域性。因此,安全的设计是将用户的文本转换为变量,然后使用任何可用的语言功能将其转换为固定字符串。但是我看到对于“int”,这可能不会被想到:我可能自己有“假设”不需要指定固定区域性的代码,如果它是一个 int 变量。intToString
18赞 Your Common Sense 9/18/2015 #2

实际上有两个问题合二为一。标题中的问题与OP在之后的评论中表达的担忧关系不大。

虽然我意识到对于 OP 来说,重要的是他们的特殊情况,但对于来自 Google 的读者来说,回答更普遍的问题很重要,这个问题可以表述为“如果我确保我连接的每个文字都是安全的,串联是否与准备好的语句一样安全?所以,我想集中谈谈后者。答案是

绝对不是。

解释并不像大多数读者想要的那么直接,但我会尽力而为。

我一直在思考这个问题一段时间,最终写了这篇文章(尽管基于 PHP 环境),我试图总结一切。我突然想到,在一些相关但更狭窄的主题中,保护 SQL 注入的问题经常被回避,例如字符串转义、类型转换等。虽然有些措施在单独采取时可以被认为是安全的,但没有系统,也没有简单的规则可以遵循。这使得它非常滑,过多地影响了开发人员的注意力和经验。

SQL注入的问题不能简化为某些特定的语法问题。它比普通开发人员过去认为的要广泛。这也是一个方法论问题。它不仅是“我们必须应用哪种特定格式”,而且还是“必须如何完成”。

(从这个角度来看,另一个答案中引用的 Jon Skeet 的一篇文章做得比好事要好得多,因为它再次在一些边缘情况下吹毛求疵,专注于特定的语法问题,而未能从整体上解决问题。

当你试图解决保护问题时,不是作为一个整体,而是作为一个不同的语法问题,你面临着许多问题。

  • 可能的格式选择列表非常庞大。意味着人们很容易忽略一些。或者混淆它们(例如,通过使用字符串转义作为标识符)。
  • 串联意味着所有保护措施必须由程序员完成,而不是程序。仅此问题就会导致以下几个后果:
    • 这种格式是手动的。手动意味着易出错。一个人可能只是忘记申请。
    • 此外,有一种诱惑是将格式化过程转移到一些集中式功能中,使事情更加混乱,并破坏不会进入数据库的数据。
  • 当涉及多个开发人员时,问题会乘以十倍。
  • 使用串联时,无法一眼看出具有潜在危险的查询:它们具有潜在危险!

与这种混乱不同,准备好的陈述确实是圣杯:

  • 它可以用一个易于遵循的简单规则的形式来表达。
  • 它本质上是不可侵犯的措施,意味着开发人员不能干涉,并且无论是否愿意,都会破坏这一过程。
  • 防止注入实际上只是准备好的语句的副作用,其真正目的是生成语法正确的语句。语法正确的语句是 100% 的注入证明。然而,尽管存在任何注入的可能性,但我们需要我们的语法是正确的。
  • 如果一直使用,无论开发人员的经验如何,它都可以保护应用程序。比如说,有一种叫做二阶注入的东西。还有一种非常强烈的错觉,上面写着“为了保护,转义所有用户提供的输入”。如果开发人员自由决定哪些需要保护,哪些不需要保护,那么它们结合在一起会导致注入。

(进一步思考,我发现当前的占位符集不足以满足现实生活的需求,必须进行扩展,无论是对于复杂的数据结构,如数组,甚至是 SQL 关键字或标识符,有时也必须动态添加到查询中,但开发人员在这种情况下没有武装, 并被迫回退到字符串连接,但这是另一个问题)。

有趣的是,这个问题的争议是由 Stack Overflow 非常有争议的性质引发的。该网站的想法是利用直接提问的用户的特定问题来实现拥有适合来自搜索的用户的通用答案数据库的目标。这个想法本身并不坏,但在这样的情况下它就失败了:当用户提出一个非常狭隘的问题时,特别是在与同事的争执中得到争论(或决定是否值得重构代码)。虽然大多数有经验的参与者都在尝试写出答案,但要牢记 Stack Overflow 的使命,让他们的答案对尽可能多的读者有好处,而不仅仅是 OP。

评论

10赞 edc65 9/19/2015
没有回答问题
0赞 Erik Hart 9/19/2015
大多数数据库通过SQL字符串相等来检测已经使用的参数化查询,因此旧的准备和使用句柄方法对我来说似乎已经过时了。这些句柄只能在特定范围内使用,并且需要编码来跟踪句柄。在我看来,参数化查询应该直接使用,这样查询计划就可以在没有句柄跟踪的情况下重用,甚至可以跨不同的应用程序重用。
102赞 Rob 9/18/2015 #3

我认为这是安全的......从技术上讲,但这是一个可怕的习惯。你真的想写这样的查询吗?

var sqlCommand = new SqlCommand("SELECT * FROM People WHERE IsAlive = " + isAlive + 
" AND FirstName = @firstName");

sqlCommand.Parameters.AddWithValue("firstName", "Rob");

在类型从整数变为字符串的情况下,它还会使您容易受到攻击(想想员工编号,尽管它的名字可能包含字母)。

因此,我们已将 EmployeeNumber 的类型从 更改为 ,但忘记更新我们的 sql 查询。哎呀。intstring

评论

24赞 RemarkLima 9/18/2015
我们可以停止使用吗?blogs.msmvps.com/jcoehoorn/blog/2014/05/12/......AddWithValue
5赞 casperOne 9/18/2015
@RemarkLima 当您动态生成将值映射到参数的代码时,解决方案是什么?这篇博文未能解决这种情况。是的,当 SQL 类型已知时,它是一行来设置的,但是当它未知时,您就会遇到问题(或者您必须求助于使用该信息注释模型)。
1赞 RemarkLima 9/18/2015
然后,除非您将数据库类型的映射作为语句动态构建的一部分,否则您将陷入困境。我假设你有一个目标列的列表,作为字典的一部分,如果你愿意,你可以拥有这些类型。否则,只需承受性能损失。归根结底,我认为这只是很好的信息。AddWithValue
8赞 casperOne 9/18/2015
@RemarkLima “我们能停止使用吗?”这句话实际上应该是“如果你知道类型,那么你应该避免使用。AddWithValueAddWithValue
2赞 RemarkLima 9/18/2015
嘿,不要射杀信使,不是我写的;-)但重点仍然存在,如果从一开始就内置到您的设计中,您就没有理由不知道类型。最佳实践和所有爵士乐:-)
7赞 Max 9/18/2015 #4

在我看来,如果你能保证你使用的参数永远不会包含字符串,那么它是安全的,但我无论如何都不会这样做。此外,由于您正在执行串联,您会看到性能略有下降。我想问你的问题是,你为什么不想使用参数?

评论

1赞 johnnyRose 9/18/2015
不是我不想使用参数,而是我在使用参数。一位同事写了这样的代码,我今天修改了这些代码以进行参数化,这让我想到了这个问题。
0赞 Max 9/18/2015
好的,太好了。使用参数是最佳做法。这样你就不必担心 sql 注入之类的事情。此外,如果要生成动态查询,则无论查询有多复杂,也可以使用参数。只需在构建它们时使用@1...@n样式即可。并将它们附加到具有所需值的参数集合中。
0赞 lerthe61 12/26/2016
@johnyRose 使用参数还有一点:程序在发展和变化。您只能对字符串使用串联,但这并不能保证有人实施更改某些参数类型的重构,并且更改可能会引入 SQL 注入漏洞。
66赞 Joel Coehoorn 9/18/2015 #5

在您控制的计算机(如 Web 服务器)上使用强类型平台时,可以阻止对仅具有 、 、 或(和其他数值)值的查询进行代码注入。令人担忧的是,由于强制 sql server 重新编译每个查询,并阻止它获得有关哪些查询以什么频率运行的良好统计信息(这会损害缓存管理)而导致的性能问题。boolDateTimeint

但是,“在您控制的计算机上”部分很重要,因为否则用户可以更改系统用于从这些值生成字符串的行为,以包含任意文本。

我也喜欢从长远考虑。当今天陈旧的强类型代码库通过自动翻译移植到新的热门动态语言时,您突然失去了类型检查,但还没有针对动态代码进行所有正确的单元测试,会发生什么?

实际上,没有充分的理由不对这些值使用查询参数。这是正确的方法。当值确实是常量时,继续将值硬编码到 sql 字符串中,但除此之外,为什么不直接使用参数呢?这并不难。

归根结底,我不会称它为虫子,但我会称它为气味:它本身就不是一个虫子,但强烈表明子就在附近,或者最终会在附近。好的代码可以避免留下异味,任何好的静态分析工具都会标记这一点。

我要补充一点,不幸的是,这不是你可以直接赢得的那种论点。这听起来像是“正确”已经不够的情况了,踩着同事的脚趾自己解决这个问题不太可能促进良好的团队动力;它最终可能弊大于利。在这种情况下,更好的方法可能是推广使用静态分析工具。这将使目标努力具有合法性和可信度,并回过头来修复现有代码。

评论

1赞 johnnyRose 9/18/2015
参数化绝对不是一个挑战。我的问题之所以出现,是因为一位同事写了一堆连接整数值的查询,我想知道浏览和修复所有这些查询是否浪费了我的时间。
2赞 johnnyRose 9/18/2015
我认为“这是一个错误”的问题就是我的问题归结为什么。
8赞 Joel Coehoorn 9/18/2015
这是一种“气味”:它本身不是虫子,但表明虫子可能在附近。好的代码试图消除气味。任何好的静态分析工具肯定会标记它。
1赞 CSS 9/24/2015
我喜欢“气味”这个词。我会用“幼虫”之类的东西来代替,它还没有完全是一个错误,但未来的更新可能会把它捕捉到一个蛆虫,它会吃掉你的后端,直到你压碎它或熏蒸它。您当然不希望在生产环境中出现坏死代码的可能性,并且拥有没有以一定技巧开发的东西肯定会导致这种情况出现,就像在这种情况下一样。
2赞 Kaspars Ozols 9/27/2015
这是错误的。请参阅我的答案,例如,您仍然可以使用 或DateTimeint
15赞 mcottle 9/18/2015 #6

我们不要只考虑安全性或类型安全的注意事项。

使用参数化查询的原因是提高数据库级别的性能。从数据库的角度来看,参数化查询是 SQL 缓冲区中的一个查询(使用 Oracle 的术语,尽管我认为所有数据库在内部都有类似的概念)。因此,数据库可以在内存中保存一定数量的查询,准备好并准备执行。这些查询不需要解析,并且速度更快。经常运行的查询通常位于缓冲区中,并且不需要在每次使用时进行分析。

除非

有人不使用参数化查询。在这种情况下,缓冲区会不断被几乎相同的查询流刷新,每个查询都需要由数据库引擎解析和运行,并且性能会受到全面影响,因为即使是频繁运行的查询最终也会在一天内重新解析多次。我以调整数据库为生,这一直是唾手可得的果实的最大来源之一。

现在

为了回答您的问题,如果您的查询具有少量不同的数值,则可能不会引起问题,实际上可能会无限地提高性能。但是,如果可能有数百个值并且查询被调用很多,您将影响系统的性能,因此不要这样做。

是的,您可以增加 SQL 缓冲区,但这最终总是以牺牲其他更关键的内存用途为代价的,例如缓存、索引或数据。道德,非常虔诚地使用参数化查询,这样你就可以优化你的数据库,并将更多的服务器内存用于重要的东西......

53赞 Maciek 9/18/2015 #7

在某些情况下,可以使用字符串值以外的非参数化(连接)变量执行SQL注入攻击 - 请参阅Jon的这篇文章:http://codeblog.jonskeet.uk/2014/08/08/the-bobbytables-culture/

问题是,当被调用时,一些自定义区域性提供程序可以将非字符串参数转换为其字符串表示形式,从而将一些 SQL 注入到查询中。ToString

评论

15赞 Arturo Torres Sánchez 9/18/2015
我认为这是唯一回答这个问题的帖子,本质上是“怎么可能用 s 进行注射?int
4赞 Martin Smith 9/20/2015
虽然,如果您能够注入自定义代码,例如诱杀装置,则很难知道为什么无论如何都需要SQL注入。CultureInfo
0赞 ken2k 9/23/2015
加 1,唯一真正回答问题的答案
0赞 user1027167 9/25/2015
@MartinSmith:请参阅我的答案,它显示了从外部更改 CultureInfo 的一种可能方法。
1赞 miken32 3/5/2020
此答案应包含链接的重要部分。
3赞 japzdivino 9/23/2015 #8

没关系,但从来都不安全。安全性始终取决于输入,例如,如果输入对象是 TextBox,攻击者可以做一些棘手的事情,因为 TextBox 可以接受字符串,因此您必须进行某种验证/转换才能防止用户输入错误。但问题是,它并不安全。就这么简单。

评论

0赞 johnnyRose 9/23/2015
不过,这是一串。我说的是其他数据类型,如整数、布尔值或日期时间。
0赞 japzdivino 10/5/2015
@johnnyRose是的,我在上面看到了一个非常好的例子,你把它标记为 Kaspards 的回答。很好的答案,因为他使用 datetime 数据类型作为不常见的示例。:)我希望你已经相信在任何类型的数据类型中使用参数都是不安全的,而且更好
0赞 johnnyRose 10/5/2015
我从来没有怀疑过使用参数更安全。我的问题涉及强类型实现。
0赞 japzdivino 10/5/2015
是的。。我同意。这也是一个很好的问题,可以帮助未来的读者:)
8赞 user1027167 9/25/2015 #9

要向 Maciek 添加一些信息,请回答:

通过反射调用程序集的 main-function,可以很容易地更改 .NET 第三方应用的区域性信息:

using System;
using System.Globalization;
using System.Reflection;
using System.Threading;

namespace ConsoleApplication2
{
  class Program
  {
    static void Main(string[] args)
    {
      Assembly asm = Assembly.LoadFile(@"C:\BobbysApp.exe");
      MethodInfo mi = asm.GetType("Test").GetMethod("Main");
      mi.Invoke(null, null);
      Console.ReadLine();
    }

    static Program()
    {
      InstallBobbyTablesCulture();
    }

    static void InstallBobbyTablesCulture()
    {
      CultureInfo bobby = (CultureInfo)CultureInfo.InvariantCulture.Clone();
      bobby.DateTimeFormat.ShortDatePattern = @"yyyy-MM-dd'' OR ' '=''";
      bobby.DateTimeFormat.LongTimePattern = "";
      bobby.NumberFormat.NegativeSign = "1 OR 1=1 OR 1=";
      Thread.CurrentThread.CurrentCulture = bobby;
    }
  }
}

仅当 BobbysApp 的 Main 函数是公共的时,这才有效。如果 Main 不是公共函数,则可以调用其他公共函数。

评论

1赞 Kaspars Ozols 9/27/2015
您甚至不必通过代码来完成。您可以直接在 Windows 区域设置中添加注入。看看我的答案。
3赞 Reza Aghaei 9/27/2015
谁可以更改服务器设置,或者谁可以在服务器中擦除此类代码?如果一个人可以在你的服务器中做到这一点,他就不需要SQL注入来破坏数据。
-3赞 Atilla Ozgur 9/27/2015 #10

不,你可以通过这种方式获得SQL注入攻击。我用土耳其语写了一篇旧文章,展示了这里是如何做到的。PHP 和 MySQL 中的文章示例,但概念在 C# 和 SQL Server 中工作相同。

基本上你按照方式攻击。假设您有一个页面,该页面根据整数 id 值显示信息。您没有将其参数化为值,如下所示。

http://localhost/sqlEnjeksiyon//instructors.aspx?id=24

好的,我假设您正在使用 MySQL,并且我按照以下方式进行攻击。

http://localhost/sqlEnjeksiyon//instructors.aspx?id=ASCII((SELECT%20DATABASE()))

请注意,此处注入的值不是字符串。我们正在使用 ASCII 函数将 char 值更改为 int。您可以使用“CAST(YourVarcharCol AS INT)”在 SQL Server 中完成相同的操作。

之后,我使用长度和子字符串函数来查找您的数据库名称。

http://localhost/sqlEnjeksiyon//instructors.aspx?id=LEN((SELECT%20DATABASE()))

http://localhost/sqlEnjeksiyon//instructors.aspx?id=ASCII(SUBSTR(SELECT%20DATABASE(),1,1))

然后使用数据库名称,开始获取数据库中的表名。

http://localhost/sqlEnjeksiyon//instructors.aspx?id=ASCII(SUBSTR((SELECT table_name FROM INFORMATION_SCHEMA.TABLES LIMIT 1),1,1))

当然,您必须自动执行此过程,因为每个查询只能获得一个字符。但是您可以轻松地将其自动化。我的文章展示了 watir 中的一个例子。仅使用一个页面,而不是参数化的 ID 值。我可以了解您数据库中的每个表名。在那之后,我可以寻找重要的表格。这需要时间,但这是可行的。

评论

2赞 johnnyRose 9/27/2015
我的问题指的是一种强类型语言。虽然您的解释非常适合具有灵活类型的语言,但注入的值仍然是一个字符串。
0赞 Atilla Ozgur 9/27/2015
注入的值均为整数。您获取 char 并使用 ASCII MySQL 函数将其更改为整数。在 SQL Server 中使用 CAST(YourCharValue AS INT) 执行相同的操作
0赞 johnnyRose 9/27/2015
我的意思是这样的:public void QuerySomething(int id) { // this will only accept an integer }
0赞 ToolmakerSteve 8/28/2020
,这不是“注入的值是整数”的示例。假定实现代码执行包含 的查询,然后将其强制转换为 int。这将是非常草率的编码,恕我直言,这不是正在讨论的内容。php 代码将输入转换为 int 的正确示例是: 观察任意输入字符串在到达查询之前被转换为 int。在向数据库发送查询之前,这种“将参数转换为 int”可以在任何语言 AFAIK 中完成。SELECT%20DATABASE()$id_value = 'ASCII((SELECT%20DATABASE())'; $param = (int)$id_value; $query = ... id={$param};
0赞 ToolmakerSteve 8/28/2020
...当然,URL get 参数字符串特别容易注入。还需要将“密钥”列入“白名单”。代码必须显式查找有效的键。在这里,我假设代码在 get 参数列表中查找键“id”。当然,将“?”后面的整个字符串复制到查询中的代码是完全不安全的。
53赞 Kaspars Ozols 9/27/2015 #11

即使对于非字符串类型,这也是安全的。始终使用参数。时期。

请考虑以下代码示例:

var utcNow = DateTime.UtcNow;
var sqlCommand = new SqlCommand("SELECT * FROM People WHERE created_on <= '" + utcNow + "'");

乍一看,代码看起来很安全,但如果在 Windows 区域设置中进行一些更改并以短日期格式添加注入,一切都会改变:

Datetime Injection

现在生成的命令文本如下所示:

SELECT * FROM People WHERE created_on <= '26.09.2015' OR '1'<>' 21:21:43'

类型也可以做同样的事情,因为用户可以定义自定义负号,可以很容易地将其更改为SQL注入。int

有人可能会争辩说应该使用不变区域性而不是当前区域性,但是我已经多次看到这样的字符串连接,并且在使用 .+

评论

10赞 Reza Aghaei 9/27/2015
谁可以更改服务器设置?如果一个人可以在你的服务器中做到这一点,他就不需要SQL注入来破坏数据。
5赞 Jeremy Thompson 9/27/2015
这是最好的答案,它显示了验证 OP 关注这是一个错误/安全漏洞的一种方法。例如,撇开性能和面向未来的不谈,在 SQL 中连接日期时间不仅仅是一种气味技术债务。@RezaAghaei这个问题从未提到过服务器端,但它可能是带有 SQLExpress 的 Windows 应用程序 - 无论哪种方式,这都不是问题的标准。任何人都可以说,但谁有权访问服务器设置来反驳这个出色的答案,就像任何人都可以说共享服务器托管或 Y2K 错误一样。我确实同意你服务器被锁定的观点 - 它只是不是一个先决条件。
2赞 johnnyRose 9/28/2015
你能举例说明你对这种类型的看法吗?int
1赞 johnnyRose 10/16/2015
我知道你回答这个问题已经几周了,但是你是否可以编辑你的帖子并添加一个如何定义自定义负号的例子?
0赞 ToolmakerSteve 8/28/2020
@RezaAghaei - 是否存在从用户的浏览器获取区域性的情况,以用户的预期格式显示?