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实现的快速排序算法详解
Aug 01 Python
Python实现的微信好友数据分析功能示例
Jun 21 Python
python3 unicode列表转换为中文的实例
Oct 26 Python
python判断列表的连续数字范围并分块的方法
Nov 16 Python
Pandas时间序列重采样(resample)方法中closed、label的作用详解
Dec 10 Python
Python面向对象封装操作案例详解 II
Jan 02 Python
python 画条形图(柱状图)实例
Apr 24 Python
python不同系统中打开方法
Jun 23 Python
Python如何读写二进制数组数据
Aug 01 Python
Django静态文件加载失败解决方案
Aug 26 Python
python中pdb模块实例用法
Jan 15 Python
pytorch 带batch的tensor类型图像显示操作
May 20 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 array的学习笔记
2012/05/10 PHP
php后退一页表单内容保存实现方法
2012/06/17 PHP
session在php5.3中的变化 session_is_registered() is deprecated in
2013/11/12 PHP
Yii2框架可逆加密简单实现方法
2017/08/25 PHP
PHP FileSystem 文件系统常用api整理总结
2019/07/12 PHP
MooTools 1.2介绍
2009/09/14 Javascript
jQuery语法总结和注意事项小结
2012/11/11 Javascript
关于jquery css的使用介绍
2013/04/18 Javascript
利用jQuary实现文字浮动提示效果示例代码
2013/12/26 Javascript
JavaScript获取XML数据附示例截图
2014/03/05 Javascript
javascript常用函数归纳整理
2014/10/31 Javascript
JavaScript模拟实现键盘打字效果
2015/06/29 Javascript
javascript数组去重的六种方法汇总
2015/08/16 Javascript
什么是JavaScript中的结果值?
2016/10/08 Javascript
jquery ajax后台返回list,前台用jquery遍历list的实现
2016/10/30 Javascript
JavaScript实现全选取消效果
2017/12/14 Javascript
vue2 中二级路由高亮问题及配置方法
2019/06/10 Javascript
vue中更改数组中属性,在页面中不生效的解决方法
2019/10/30 Javascript
微信小程序12行js代码自己写个滑块功能(推荐)
2020/07/15 Javascript
vue 数据操作相关总结
2020/12/17 Vue.js
Python内置函数bin() oct()等实现进制转换
2012/12/30 Python
探索Python3.4中新引入的asyncio模块
2015/04/08 Python
利用Python如何生成随机密码
2016/04/20 Python
Python PyQt5实现的简易计算器功能示例
2017/08/23 Python
python list格式数据excel导出方法
2018/10/31 Python
Python设计模式之原型模式实例详解
2019/01/18 Python
python字典setdefault方法和get方法使用实例
2019/12/25 Python
Python Matplotlib简易教程(小白教程)
2020/07/28 Python
为您的家、后院、车库等在线购物:Spreetail
2019/06/17 全球购物
奥地利领先的在线药房:SHOP APOTHEKE
2019/10/07 全球购物
自荐信的禁忌和要点
2013/10/15 职场文书
关于中国梦的演讲稿
2014/04/23 职场文书
正科级干部考察材料
2014/05/29 职场文书
学习考察心得体会
2014/09/04 职场文书
观后感格式
2015/06/19 职场文书
幼儿园开学家长寄语(2016春季)
2015/12/03 职场文书