Django中和时区相关的安全问题详解


Posted in Python onOctober 12, 2020

在开发国际化网站的时候,难免会与时区打交道,通用CMS更是如此,毕竟其潜在用户可能是来自于全球各地的。Django在时区这个问题上下了不少功夫,但是很多资深的开发者都有可能尚未完全屡清楚Django中各种时间的实际意义和使用方法,导致写出错误的代码;作为安全研究人员,时区问题也可能和一些安全问题挂钩,比如优惠券的过期时间、订单的下单与取消时间等,如果没有考虑时区问题,有可能将导致一些逻辑漏洞。

本文就从多个常用模块开始,了解一下Django中的时区究竟是怎么回事,以及在时间的比较中可能出现的一些逻辑错误。

从“两种时间”说起

我们都知道,在Python中表示“时间”的对象是datetime.datetime

其实在Python中,这个对象被分成了两个类型:

  • aware datetime
  • naive datetime

他们的区别是:如果datetime对象的tzinfo属性有设置时区值,则这个对象是一个aware datime;否则它是一个naive datetime。

举个例子,我们平时在编写Python脚本的时候,使用下面这行代码获取当前时间:

from datetime import datetime
t = datetime.now()

此时,t是一个naive datetime,因为我们没有给他设置时区:

Django中和时区相关的安全问题详解

naive的中文意思大家应该都很熟悉,这里的大概意思就是“simple”,这是一个很简单、原始的时间对象。实际上就是指,计算机不知道这个时间,他的时区究竟是什么,它可能代表着北京时间,也可能是UTC时间,因为我们没有指定时区,我们无法“假设”其是计算机系统所在的时区,也无法“假设”其是UTC时区。也就是说,计算机拿到了一个naive datetime,是无法准确地定位到某一个时间点的,也无法直接转换成一个unix时间戳。

那么相对的,aware datetime就是计算机能准确知道其时区的时间对象,他是一个准确的时间点,就落在时间轴上的某个地方,不管从哪个时区看,这个点都是绝对固定的。所以,我们可以将一个aware datetime转换成unix时间戳。

有的同学可能比较好奇,你说naive datetime无法转换成时间戳,那么为什么这个对象有一个timestamp()方法呢:

Django中和时区相关的安全问题详解

原因我们查文档可以得出结论,如果对象是naive datetime,则会以当前系统本地时区为准。

Django的时区配置

回到Django。由于Django是一个国际化框架,时区相关处理自然是其必不可少的组成部分。Django的配置项中,有下面两个选项与时区相关:

  • USE_TZ
  • TIME_ZONE

USE_TZ用来指定整个项目是否使用时区TIME_ZONE是默认时区的值。

如果USE_TZ的值设置为False,那么Django项目中所有时间都使用naive datetime(除非有明确指定时区的情况)。也就是说,网站内存储和使用的时间全部是TIME_ZONE的值所指定的时区。

这样做有一些弊端:

  • 数据库中保存的是naive datetime,导致在跨区域迁移数据的时候,可能无法准确定位到某个时间点
  • 国际化企业可能面向不同国家有不同的网站,但后台数据库相同,此时究竟使用哪个时区保存和展示时间,将引起混乱
  • 即使是同一个网站的用户,他们可能来自于全球各地,查看到的时间却是统一的服务器时间,对于高交互式的应用十分不友好
  • 即使网站面向的用户仅来自于某一个地区,也会涉及到“夏时令”(Daylight Saving Time)相关的问题,每年可能将会导致两次时间误差

默认情况下,用django-admin生成的项目,其设置中USE_TZ等于True,这也是Django官方建议的配置。此时,在网站内部存储与使用的是UTC时间,而与用户交互时使用TIME_ZONE或手工的时区。

我们后文中也以Django的默认配置USE_TZ=True为前提条件,否则也没有讨论的必要了。

Django的时间函数

Django的包django.utils.timezone中有下面几个常用的时间相关函数:

  • now(),返回当前的UTC时间
  • localtime(),返回当前的本地时间(默认是TIME_ZONE配置指定的时区时间)
  • is_aware(),传入的时间是否是aware datetime
  • is_naive(),传入的时间是否是naive datetime
  • make_aware(),将naive时间转换成aware时间
  • make_naive(),将aware时间转换成naive时间

因为开启了USE_TZ,Django内部操作时间时都应该使用aware时间,否则会出现异常。所以,我们在获取当前时间的时候,一定要使用Django自带的now()localtime()函数,而不能使用Python的datetime.datetime.now()函数。

数据库存储的时间

我们在使用ORM的DatetimeField时,常常会有这样的疑虑:我们究竟应该给DatetimeField传入哪个时区的时间呢?

可以做个试验,编写下面这个model:

class Archive(models.Model):
  title = models.CharField('title', max_length=256)

  now_time = models.DateTimeField(default=timezone.now)
  local_time = models.DateTimeField(default=timezone.localtime)

这个model有三个属性,title是他的名字,now_time和local_time是两个时间,他们的默认值分别是timezone.now和timezone.localtime。

也就是说,默认情况下,now_time字段传入的是UTC时区的当前时间,local_time字段传入的是本地时区的当前时间,我这里是Asia/Shanghai

然后,我们创建一个Archive对象:

Django中和时区相关的安全问题详解

可以发现,不管我们使用a.now_time还是a.local_time,读取到的datetime对象的tzinfo都是UTC。

这也印证了Django文档中说到的,不管传入的时间对象时区是什么,其内部存储的时间均为UTC时区。但是,值得注意的是,如果我们传入了一个不带时区的naive datetime,将会出现一个警告,并使用默认时区填充其tzinfo:

Django中和时区相关的安全问题详解

模板中展示的时间

对于网站的用户来说,他们想看到的时间显然不是UTC时间,而是某一个具体时区的时间。比如,我的网站几乎全部是中国用户,那么展示时使用的时区应该是Asia/Shanghai

这一部分的转换,Django放在的模板引擎中。

Django在渲染模板变量时,将会遇到两种与时间有关的情况:

<p>origin value: {{ object.now_time }}</p>
<p>date filter: {{ object.now_time | date:'Y-m-d H:i:s' }}</p>

前者是直接将时间渲染到页面中,后者是通过date这样的模板filter处理后渲染在页面中。这两种情况在内部处理方式略有不同此处不细表,总体而言,任意模板中变量的渲染,都会被转换时区。

那么,脱离模板引擎,我们会得到怎样的结果呢?

在流行的前后端分离架构中,后端服务器通常只提供JSON格式的接口给前端,那么,我们编写下面这样一个view,看看返回值是什么:

from django.shortcuts import get_object_or_404
from django.http.response import JsonResponse
from django.utils import timezone

from . import models


def json(request):
  object = get_object_or_404(models.Archive, pk=1)
  data = dict(
    id=object.pk,
    now_time=object.now_time,
    local_time=timezone.localtime(object.local_time)
  )
  return JsonResponse(data=data)

返回对象的now_time,我直接将object.now_time返回;返回对象的local_time,我将数据库值转换成本地时间timezone.localtime(object.local_time)返回。

我前文说过,这两个值在数据库中的值是完全相等的,不过在json返回中,now_time是UTC时间,而local_time是北京时间:

Django中和时区相关的安全问题详解

也就是说,在前后端分离的网站中,如果直接使用Model的字段,那么前端需要负责进行时区的转换,否则将会出现时间的偏差。

时间的校验和比较

在一些业务场景下,我们可能会涉及到时间的校验和比较,如:

  • 付费服务、商品、用户的有效期检查
  • 活动的开始与结束时间检查
  • 订单、商品的收货、取消时间检查

我们就以付费用户为例:用户购买了30天的VIP会员,我们需要给用户表中设置一个过期时间,比如下面这个model。

from django.db import models
from django.utils import timezone

class Account(models.Model):
  username = models.CharField(max_length=256)
  password = models.CharField(max_length=64)

  created_time = models.DateTimeField(default=timezone.now)
  expired_time = models.DateTimeField()

如果某个用户某一个时刻对网站进行访问,我们如何判断他是否具有VIP权限呢?

通常情况下我们有两种常见的判断方法。一是,用户访问时,直接从model中取出这个对象,然后和now()进行比较:

Django中和时区相关的安全问题详解

这种情况下,当前时间不管是now()还是localtime()都不影响比较的结果,因为两个datetime对象在比较时会考虑时差。

另一种情况是,通过ORM的queryset进行比较,等于在数据库层面进行操作:

if models.Account.objects.filter(expired_time__gt=timezone.now()).exists():
  # doing sth

Django中和时区相关的安全问题详解

Django也帮我们考虑过这种情况,即使此时我们使用本地时间timezone.localtime()进行查询,系统也会将其转换成UTC时间传入SQL语句:

Django中和时区相关的安全问题详解

但是,如果我们使用到了和日期、时间有关的lookups,将产生相反的结果。

怎么理解这个问题呢,我们还是来举个例子。比如,网站以用户注册当天的日子作为“会员日”(比如1月2日注册的会员,以后每月的2日都是他的会员日),会员日这一天会给这个用户赠送优惠券。

那么,发送优惠券时,我们如何筛选网站内会员日是今日的所有用户?

下面这个filter是否正确?

models.Account.objects.filter(created_time__day=timezone.now().day).all()

答案是否定的,我们应该使用timezone.localtime()表示今天,而非timezone.now()

models.Account.objects.filter(created_time__day=timezone.localtime().day).all()

这是为什么呢?你不是说数据库中存储的都是UTC时间吗,为何会使用到timezone.localtime()

原因是,Django在使用日期、时间有关的lookups时,会在数据库层面对时间进行时区的转换再进行比较,所以我们需要使用本地时间而不是UTC时间。

可以看看原始的SQL语句:

Django中和时区相关的安全问题详解

可见,SQL语句中使用了django_datetime_extract('day', "sample_account"."created_time", 'Asia/Shanghai', 'UTC')将UTC时间转换成了北京时间,因此后面比较的时候,也应该使用北京时间。

这一点需要格外注意。时间比较的不谨慎,说小点是一个Bug,说大点就是漏洞,毕竟很多涉及到时间比较的情景,都是非常需要严谨的。

所以,我们总结一下:

  • 任何比较都使用aware时间,不能使用naive时间
  • 时间属性直接比较时,使用任何aware时间均可(会被自动转换成UTC)
  • queryset查询,不涉及__day、__date、__year等时间lookups时,使用任何aware时间均可(会被自动转换成UTC)
  • queryset查询,涉及到时间lookups时,使用本地时间

到此这篇关于Django中和时区相关的安全问题详解的文章就介绍到这了,更多相关Django时区安全问题内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Python 相关文章推荐
Python浅拷贝与深拷贝用法实例
May 09 Python
Python实现批量下载图片的方法
Jul 08 Python
Python如何快速上手? 快速掌握一门新语言的方法
Nov 14 Python
python3+PyQt5使用数据库表视图
Apr 24 Python
python多进程使用及线程池的使用方法代码详解
Oct 24 Python
python 实现语音聊天机器人的示例代码
Dec 02 Python
python random从集合中随机选择元素的方法
Jan 23 Python
python从子线程中获得返回值的方法
Jan 30 Python
pytorch多GPU并行运算的实现
Sep 27 Python
python将数组n等分的实例
Dec 02 Python
python中if及if-else如何使用
Jun 02 Python
Python 必须了解的5种高级特征
Sep 10 Python
python调用有道智云API实现文件批量翻译
Oct 10 #Python
python线程池 ThreadPoolExecutor 的用法示例
Oct 10 #Python
python开发一款翻译工具
Oct 10 #Python
Python pickle模块常用方法代码实例
Oct 10 #Python
Python3.9新特性详解
Oct 10 #Python
Python random模块的使用示例
Oct 10 #Python
python 装饰器的使用示例
Oct 10 #Python
You might like
php面向对象全攻略 (十) final static const关键字的使用
2009/09/30 PHP
PHP的博客ping服务代码
2012/02/04 PHP
PHP检测用户是否关闭浏览器的方法
2016/02/14 PHP
php实现给二维数组中所有一维数组添加值的方法
2017/02/04 PHP
php 中的closure用法详解
2017/06/12 PHP
Yii框架Session与Cookie使用方法示例
2019/10/14 PHP
JavaScript 创建对象
2009/07/17 Javascript
也说JavaScript中String类的replace函数
2011/09/22 Javascript
javascript实现PC网页里的拖拽效果
2016/03/14 Javascript
js验证框架实现代码分享
2016/05/18 Javascript
js 创建对象 经典模式全面了解
2016/08/16 Javascript
微信小程序 SocketIO 实例讲解
2016/10/13 Javascript
使用Node.js给图片加水印的方法
2016/11/15 Javascript
关于axios返回空对象的问题解决
2017/04/04 Javascript
基于twbsPagination.js分页插件使用心得(分享)
2017/10/21 Javascript
vue在手机中通过本机IP地址访问webApp的方法
2018/08/15 Javascript
详解vue中async-await的使用误区
2018/12/05 Javascript
JS面向对象之单选框实现
2020/01/17 Javascript
python读写二进制文件的方法
2015/05/09 Python
浅析使用Python操作文件
2017/07/31 Python
Python使用itertools模块实现排列组合功能示例
2018/07/02 Python
Django添加sitemap的方法示例
2018/08/06 Python
Python面向对象程序设计OOP入门教程【类,实例,继承,重载等】
2019/01/05 Python
常用的10个Python实用小技巧
2020/08/10 Python
python 5个顶级异步框架推荐
2020/09/09 Python
让IE6、IE7、IE8支持CSS3的脚本
2010/07/20 HTML / CSS
HTML5 文件上传下载的实例代码
2017/07/03 HTML / CSS
草莓网化妆品澳大利亚站:Strawberrynet AU
2017/12/18 全球购物
标记环介质访问控制协议
2016/03/27 面试题
J2EE是技术还是平台还是框架
2016/08/14 面试题
优秀会计求职信
2014/07/04 职场文书
2014初中数学教研组工作总结
2014/12/19 职场文书
生日答谢词
2015/01/05 职场文书
起诉书格式范文
2015/05/20 职场文书
电影雨中的树观后感
2015/06/15 职场文书
CSS实现五种常用的2D转换
2021/12/06 HTML / CSS