Subject: bin/28450: date(1) does not validate its input and accepts and processes impossible dates
To: None <gnats-admin@netbsd.org, netbsd-bugs@netbsd.org>
From: None <dhgutteridge@sympatico.ca>
List: netbsd-bugs
Date: 11/29/2004 03:45:00
>Number:         28450
>Category:       bin
>Synopsis:       date(1) does not validate its input and accepts and processes impossible dates
>Confidential:   no
>Severity:       serious
>Priority:       medium
>Responsible:    bin-bug-people
>State:          open
>Class:          sw-bug
>Submitter-Id:   net
>Arrival-Date:   Mon Nov 29 03:45:00 +0000 2004
>Originator:     David H. Gutteridge
>Release:        1.6.1/macppc and 2.0_RC4/i386
>Organization:
>Environment:
>Description:
The date(1) command does not validate its input and accepts impossible values like the 17th month of a year.  I stumbled upon this while hurredly trying to change a date, which I (unthinkingly) supplied in day/month order (as we customarily think up here in Canada), e.g. 17111645.00, which date accepts and changes the system date to the 11th of May of the next year.

From looking at the revisions in CVS, I can see that the original UCB source did in fact check for invalid input, although not in an entirely thorough manner.  As of revision 1.20, someone removed this code, indicating that mktime(3) does date validation.  However, mktime will only give an error if it cannot derive a date from what it's supplied, the input it accepts extends beyond what a user would reasonably expect in the context of the date command.

I had a look at what OpenBSD and FreeBSD have as well, they left the original UCB validation code in, and in OpenBSD's case they enhanced it a bit, but they both still miss things.

I have supplied a proposed patch which (hopefully) idiot-proofs the date command, and gives descriptive errors so frequently sleep-deprived sorts like myself are told exactly what they did wrong (assuming they didn't do more than one thing wrong).

Comments about my (pretty trivial) patch:
(1) I created a new function to report these errors to be as precise as possible, and to keep them distinct from a general "bad format", and technically they're not "illegal times".
(2) I added a check to the century as well because it's more efficient than letting it get caught by mktime, and for consistency's sake.

>How-To-Repeat:
Enter an invalid date such as 17111645.00
>Fix:
--- date.c.orig	Thu Aug  7 09:05:09 2003
+++ date.c	Sun Nov 28 21:55:09 2004
@@ -68,6 +68,7 @@
 int main(int, char *[]);
 static void badformat(void);
 static void badtime(void);
+static void badvalue(const char *);
 static void setthetime(const char *);
 static void usage(void);
 
@@ -138,6 +139,13 @@
 	/* NOTREACHED */
 }
 
+static void
+badvalue(const char * param)
+{
+	warnx("invalid %s supplied", param);
+	usage();
+}
+
 #define ATOI2(s) ((s) += 2, ((s)[-2] - '0') * 10 + ((s)[-1] - '0'))
 
 static void
@@ -146,7 +154,7 @@
 	struct timeval tv;
 	struct tm *lt;
 	const char *dot, *t;
-	int len, yearset;
+	int fullyear, len, yearset;
 
 	for (t = p, dot = NULL; *t; ++t) {
 		if (isdigit((unsigned char)*t))
@@ -168,6 +176,8 @@
 			badformat();
 		++dot;
 		lt->tm_sec = ATOI2(dot);
+		if (lt->tm_sec > 61)
+			badvalue("seconds");
 	} else {
 		len = 0;
 		lt->tm_sec = 0;
@@ -177,6 +187,8 @@
 	switch (strlen(p) - len) {
 	case 12:				/* cc */
 		lt->tm_year = ATOI2(p) * 100 - TM_YEAR_BASE;
+		if (lt->tm_year < 0)
+			badtime();
 		yearset = 1;
 		/* FALLTHROUGH */
 	case 10:				/* yy */
@@ -192,16 +204,50 @@
 		/* FALLTHROUGH */
 	case 8:					/* mm */
 		lt->tm_mon = ATOI2(p);
+		if (lt->tm_mon > 12 || lt->tm_mon == 0)
+			badvalue("month");
 		--lt->tm_mon;			/* time struct is 0 - 11 */
 		/* FALLTHROUGH */
 	case 6:					/* dd */
 		lt->tm_mday = ATOI2(p);
+		switch (lt->tm_mon) {
+		case 0:
+		case 2:
+		case 4:
+		case 6:
+		case 7:
+		case 9:
+		case 11:
+			if (lt->tm_mday > 31 || lt->tm_mday == 0)
+				badvalue("day of month");
+			break;
+		case 3:
+		case 5:
+		case 8:
+		case 10:
+			if (lt->tm_mday > 30 || lt->tm_mday == 0)
+				badvalue("day of month");
+			break;
+		case 1:
+			fullyear = lt->tm_year + TM_YEAR_BASE;
+			if (lt->tm_mday > 29 || lt->tm_mday == 0 ||
+			    lt->tm_mday == 29 && (fullyear % 4 != 0 ||
+			     fullyear % 100 == 0 && fullyear % 400 != 0))
+				badvalue("day of month");
+			break;
+		default:
+			badvalue("month");
+		}
 		/* FALLTHROUGH */
 	case 4:					/* hh */
 		lt->tm_hour = ATOI2(p);
+		if (lt->tm_hour > 23)
+			badvalue("hour");
 		/* FALLTHROUGH */
 	case 2:					/* mm */
 		lt->tm_min = ATOI2(p);
+		if (lt->tm_min > 59)
+			badvalue("minute");
 		break;
 	default:
 		badformat();