Opened 3 months ago

Closed 2 months ago

#1368 closed defect (fixed)

Date oveflow problems with ngx_gmtime()

Reported by: Jamie-Landeg-Jones@… Owned by:
Priority: minor Milestone:
Component: nginx-core Version: 1.12.x
Keywords: epoch negative date ngx_gmtime Cc:
Sensitive: no
uname -a: FreeBSD lapcat.dyslexicfish.net 11.1-STABLE FreeBSD 11.1-STABLE #0: Thu Jul 27 19:27:36 BST 2017 root@lapcat.dyslexicfish.net:/usr/obj/usr/src/sys/LAPCAT amd64
nginx -V: nginx version: nginx/1.12.1 built with OpenSSL 1.0.2l 25 May 2017 TLS SNI support enabled configure arguments: --prefix=/usr/local/etc/nginx --with-cc-opt='-I /usr/local/include' --with-ld-opt='-L /usr/local/lib' --conf-path=/usr/local/etc/nginx/nginx.conf --sbin-path=/usr/local/sbin/nginx --pid-path=/var/run/nginx.pid --error-log-path=/var/log/nginx/error.log --user=www --group=www --modules-path=/usr/local/libexec/nginx --with-file-aio --http-client-body-temp-path=/var/tmp/nginx/client_body_temp --http-fastcgi-temp-path=/var/tmp/nginx/fastcgi_temp --http-proxy-temp-path=/var/tmp/nginx/proxy_temp --http-scgi-temp-path=/var/tmp/nginx/scgi_temp --http-uwsgi-temp-path=/var/tmp/nginx/uwsgi_temp --http-log-path=/var/log/nginx/access.log --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gzip_static_module --with-http_gunzip_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_stub_status_module --with-http_sub_module --with-pcre --with-http_v2_module --with-stream=dynamic --with-stream_ssl_module --with-stream_ssl_preread_module --with-threads --with-mail=dynamic --without-mail_imap_module --without-mail_pop3_module --without-mail_smtp_module --with-mail_ssl_module --with-http_ssl_module

Description

There is a problem in ngx_gmtime() with dates earlier than the standard epoch, such that the negative number is treated as an unsigned integer, causing the year field to be 5 characters larger than allocated for, and hence the "(ngx_pool_t)*pool" variable to overflow by 5 characters. (E.G. "Last-Modified: Thu, 09 Nov 438498967 05:59:16 GMT")

Now, I don't know if this overflow into "(ngx_buf_t)*header_in" can be exploited, but still, I think it should be avoided.

Now, I appreciate that you aren't concerned with "negative" dates, which is fair enough, but may I at least suggest setting such dates to zero rather than simply casting then onto an unsigned type, as in the attached patch?

01:58 (452) "nginx-1.12.1" jamie@lapcat% l -T /scratch/jamie/ports_base/usr/local/www/nginx-dist/index.html
4 -rw-r--r-- 1 jamie jamie - 4 31 Dec 23:59:00 1969 /scratch/jamie/ports_base/usr/local/www/nginx-dist/index.html

BEFORE PATCH:

01:58 (453) "nginx-1.12.1" jamie@lapcat% curl -vvv localhost:55555

  • Rebuilt URL to: localhost:55555/
  • Connected to localhost (127.0.0.1) port 55555 (#0)

    GET / HTTP/1.1
    Host: localhost:55555
    User-Agent: curl/7.55.1
    Accept: */*

< HTTP/1.1 200 OK
< Server: nginx/1.12.1
< Date: Mon, 28 Aug 2017 00:58:42 GMT
< Content-Type: text/html
< Content-Length: 4
< Last-Modified: Thu, 09 Nov 438498967 05:59:16 GMT
< Connection: keep-alive
< ETag: "-e4c-4"
< Accept-Ranges: bytes
<
boo

  • Connection #0 to host localhost left intact

AFTER PATCH:

02:02 (461) "nginx-1.12.1" jamie@lapcat% curl -vvv localhost:55555

  • Rebuilt URL to: localhost:55555/
  • Trying 127.0.0.1...
  • Connected to localhost (127.0.0.1) port 55555 (#0)

    GET / HTTP/1.1
    Host: localhost:55555
    User-Agent: curl/7.55.1
    Accept: */*

< HTTP/1.1 200 OK
< Server: nginx/1.12.1
< Date: Mon, 28 Aug 2017 01:02:17 GMT
< Content-Type: text/html
< Content-Length: 4
< Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT
< Connection: keep-alive
< ETag: "-e4c-4"
< Accept-Ranges: bytes
<
boo

  • Connection #0 to host localhost left intact

Cheers, Jamie

Attachments (1)

patch-src_core_ngx__times.c (349 bytes) - added by Jamie-Landeg-Jones@… 3 months ago.

Download all attachments as: .zip

Change History (7)

Changed 3 months ago by Jamie-Landeg-Jones@…

comment:1 Changed 3 months ago by mdounin

This used to result in 2038..2106 dates on 32-bit platforms, though certainly need some tweaking for 64-bit. Thanks for reporting this.

I don't think that using Epoch for negative timestamps is enough though. Positive values can result in years longer than 4 digits as well, and the result will be the same. So we should either allocate enough space in all relevant places, or somehow reject/truncate dates after 9999 year.

comment:2 Changed 3 months ago by Jamie-Landeg-Jones@…

Hiya. No problem!

Arrrgh! Good call! Of course, you're right that you can overflow the 4 digit year with positive numbers too. I'm so stupid... Always focusing on the negative! :-)

Yep, I've seen one or two non-nginx programs hiccup with 64bit u_time, especially with a pre-epoch date.

I must admit, I was surprised that in ngx_times.c where the string itself was created, there was no bound-checking on the character length of the string components, however I'm guessing the reason for that (and also not using strftime) is efficiency. I know nginx has a great reputation for performance, and it would just be a waste of processing cycles bound checking something you can (ordinarily!) rely on to be safe.

Anyway, truncating times after year 9999 sounds good, though you do realise that people will be blaming you for the Y10K problem! ha-ha!

How about something like this? :

--- src/core/ngx_times.c.orig   2017-07-11 14:24:07.000000000 +0100
+++ src/core/ngx_times.c        2017-08-30 07:52:08.403273000 +0100
@@ -304,7 +304,7 @@

     /* the calculation is valid for positive time_t only */

-    n = (ngx_uint_t) t;
+    n = (ngx_uint_t) t & 0xfffffffff;

     days = n / 86400;

That mask will force it to be positive, and also drop the higher bits, meaning the upper limit will be in the 4 digit-year:

Last-Modified: Sun, 20 Aug 4147 07:32:14 GMT

Of course, that will also be the same as "-1" so you may still want to still have the < 0 check before applying the mask, to make the output less confusing at least when a negative date is encountered, although I personally wouldn't care for dates before 1970 or after 4147 other than for enforcing bound checking!

I'm assuming this wouldn't be considered a kludge, or non-portable solution? I'm willing to take the blame for the Y4.147K problem when it occurs :-)

Anyway, just throwing out a few thoughts.
Cheers, Jamie

comment:3 Changed 3 months ago by mdounin

Well, I don't think that introducing Y4.147K problem is a good idea. Rather, I would stick to Y10K one, which is already in various date formats nginx uses. Here are couple of patches which add truncation to December 31, 9999 and also fix ngx_gmtime() on 32-bit platforms with 64-bit time_t.

# HG changeset patch
# User Maxim Dounin <mdounin@mdounin.ru>
# Date 1504058302 -10800
#      Wed Aug 30 04:58:22 2017 +0300
# Node ID 22bb74db6e82cc47feb1f516bb78240dadfd85c9
# Parent  9bd5c23f0ed006c904263e3b3c716e4f4a214668
Fixed ngx_gmtime() on 32-bit platforms with 64-bit time_t.

In ngx_gmtime(), instead of casting to ngx_uint_t we now work with
time_t directly.  This allows using dates after 2038 on 32-bit platforms
which use 64-bit time_t, notably NetBSD and OpenBSD.

As the code is not able to work with negative time_t values, argument
is now set to 0 for negative values.  As a positive side effect, this
results in Epoch being used for such values instead of a date in distant
future.

diff --git a/src/core/ngx_times.c b/src/core/ngx_times.c
--- a/src/core/ngx_times.c
+++ b/src/core/ngx_times.c
@@ -300,23 +300,25 @@ void
 ngx_gmtime(time_t t, ngx_tm_t *tp)
 {
     ngx_int_t   yday;
-    ngx_uint_t  n, sec, min, hour, mday, mon, year, wday, days, leap;
+    ngx_uint_t  sec, min, hour, mday, mon, year, wday, days, leap;
 
     /* the calculation is valid for positive time_t only */
 
-    n = (ngx_uint_t) t;
+    if (t < 0) {
+        t = 0;
+    }
 
-    days = n / 86400;
+    days = t / 86400;
+    sec = t % 86400;
 
     /* January 1, 1970 was Thursday */
 
     wday = (4 + days) % 7;
 
-    n %= 86400;
-    hour = n / 3600;
-    n %= 3600;
-    min = n / 60;
-    sec = n % 60;
+    hour = sec / 3600;
+    sec %= 3600;
+    min = sec / 60;
+    sec %= 60;
 
     /*
      * the algorithm based on Gauss' formula,
# HG changeset patch
# User Maxim Dounin <mdounin@mdounin.ru>
# Date 1504059349 -10800
#      Wed Aug 30 05:15:49 2017 +0300
# Node ID 2427016f24617a379c3b891396fae6b01d49381f
# Parent  22bb74db6e82cc47feb1f516bb78240dadfd85c9
Introduced time truncation to December 31, 9999 (ticket #1368).

Various buffers are allocated in an assumption that there would be
no more than 4 year digits.  This might not be true on platforms
with 64-bit time_t, as 64-bit time_t is able to represent more than that.
Such dates with more than 4 year digits hardly make sense though, as
various date formats in use does not allow them anyway.

As such, all dates are now truncated by ngx_gmtime() to December 31, 9999.
This should have no effect on valid dates, though will prevent potential
buffer overflows on invalid ones.

diff --git a/src/core/ngx_times.c b/src/core/ngx_times.c
--- a/src/core/ngx_times.c
+++ b/src/core/ngx_times.c
@@ -311,6 +311,16 @@ ngx_gmtime(time_t t, ngx_tm_t *tp)
     days = t / 86400;
     sec = t % 86400;
 
+    /*
+     * no more than 4 year digits supported,
+     * truncate to December 31, 9999, 23:59:59
+     */
+
+    if (days > 2932896) {
+        days = 2932896;
+        sec = 86399;
+    }
+
     /* January 1, 1970 was Thursday */
 
     wday = (4 + days) % 7;

comment:4 Changed 3 months ago by Jamie-Landeg-Jones@…

Yeah, looks good! And much cleaner than my hack!

Incidentally, thanks for the quick and thorough response - sadly, too many projects seem to never even look at bug reports.

cheers, jamie

Last edited 3 months ago by Jamie-Landeg-Jones@… (previous) (diff)

comment:5 Changed 2 months ago by Maxim Dounin <mdounin@…>

In 7104:cdbcb73239ee/nginx:

Introduced time truncation to December 31, 9999 (ticket #1368).

Various buffers are allocated in an assumption that there would be
no more than 4 year digits. This might not be true on platforms
with 64-bit time_t, as 64-bit time_t is able to represent more than that.
Such dates with more than 4 year digits hardly make sense though, as
various date formats in use do not allow them anyway.

As such, all dates are now truncated by ngx_gmtime() to December 31, 9999.
This should have no effect on valid dates, though will prevent potential
buffer overflows on invalid ones.

comment:6 Changed 2 months ago by mdounin

  • Resolution set to fixed
  • Status changed from new to closed

Patches were reviewed and committed, thanks.

Note: See TracTickets for help on using tickets.