Skip to content

Commit a3d5d2c

Browse files
authored
Merge pull request #1345 from denislevin/denisl/cs/MishandlingJapaneseDatesAndLeapYear
C#: Japanese Era and Leap Year checks (Likely Bugs)
2 parents 3c9c0e9 + aab4351 commit a3d5d2c

File tree

13 files changed

+332
-0
lines changed

13 files changed

+332
-0
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>When creating a <code>System.DateTime</code> object by setting the year, month, and day in the constructor by performing an arithmetic operation on a different <code>DateTime</code> object, there is a risk that the date you are setting is invalid.</p>
7+
<p>On a leap year, such code may throw an <code>ArgumentOutOfRangeException</code> with a message of <code>"Year, Month, and Day parameters describe an unrepresentable DateTime."</code></p>
8+
</overview>
9+
<recommendation>
10+
<p>Creating a <code>System.DateTime</code> object based on a different <code>System.DateTime</code> object, use the appropriate methods to manipulate the date instead of arithmetic.</p>
11+
</recommendation>
12+
<example>
13+
<p>In this example, we are incrementing/decrementing the current date by one year when creating a new <code>System.DateTime</code> object. This may work most of the time, but on any given February 29th, the resulting value will be invalid.</p>
14+
<sample src="UnsafeYearConstructionBad.cs" />
15+
<p>To fix this bug, we add/substract years to the current date by calling <code>AddYears</code> method on it.</p>
16+
<sample src="UnsafeYearConstructionGood.cs" />
17+
</example>
18+
<references>
19+
<li>
20+
<a href="https://docs.microsoft.com/en-us/dotnet/api/system.datetime?view=netframework-4.8">System.DateTime Struct</a>.
21+
</li>
22+
</references>
23+
</qhelp>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* @name Unsafe year argument for 'DateTime' constructor
3+
* @description Constructing a 'DateTime' struct by setting the year argument to an increment or decrement of the year of a different 'DateTime' struct
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @id cs/unsafe-year-construction
7+
* @precision high
8+
* @tags security
9+
* date-time
10+
* reliability
11+
*/
12+
13+
import csharp
14+
import DataFlow::PathGraph
15+
import semmle.code.csharp.dataflow.TaintTracking
16+
17+
class UnsafeYearCreationFromArithmeticConfiguration extends TaintTracking::Configuration {
18+
UnsafeYearCreationFromArithmeticConfiguration() {
19+
this = "UnsafeYearCreationFromArithmeticConfiguration"
20+
}
21+
22+
override predicate isSource(DataFlow::Node source) {
23+
exists(ArithmeticOperation ao, PropertyAccess pa | ao = source.asExpr() |
24+
pa = ao.getAChild*() and
25+
pa.getProperty().getQualifiedName().matches("System.DateTime.Year")
26+
)
27+
}
28+
29+
override predicate isSink(DataFlow::Node sink) {
30+
exists(ObjectCreation oc |
31+
sink.asExpr() = oc.getArgumentForName("year") and
32+
oc.getObjectType().getABaseType*().hasQualifiedName("System.DateTime")
33+
)
34+
}
35+
}
36+
37+
from UnsafeYearCreationFromArithmeticConfiguration config, DataFlow::PathNode source, DataFlow::PathNode sink
38+
where config.hasFlowPath(source, sink)
39+
select sink, source, sink, "This $@ based on a 'System.DateTime.Year' property is used in a construction of a new 'System.DateTime' object, flowing to the 'year' argument.", source, "arithmetic operation"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
public class UnsafeYearConstructionBad
3+
{
4+
public UnsafeYearConstructionBad()
5+
{
6+
DateTime Start;
7+
DateTime End;
8+
var now = DateTime.UtcNow;
9+
// the base-date +/- n years may not be a valid date.
10+
Start = new DateTime(now.Year - 1, now.Month, now.Day, 0, 0, 0, DateTimeKind.Utc);
11+
End = new DateTime(now.Year + 1, now.Month, now.Day, 0, 0, 1, DateTimeKind.Utc);
12+
}
13+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System;
2+
public class UnsafeYearConstructionGood
3+
{
4+
public UnsafeYearConstructionGood()
5+
{
6+
DateTime Start;
7+
DateTime End;
8+
var now = DateTime.UtcNow;
9+
Start = now.AddYears(-1).Date;
10+
End = now.AddYears(-1).Date.AddSeconds(1);
11+
}
12+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System;
2+
using System.Globalization;
3+
public class Example
4+
{
5+
public static void Main()
6+
{
7+
var cal = new JapaneseCalendar();
8+
// constructing date using current era
9+
var dat = cal.ToDateTime(2, 1, 2, 0, 0, 0, 0);
10+
Console.WriteLine($"{dat:s}");
11+
// constructing date using current era
12+
dat = new DateTime(2, 1, 2, cal);
13+
Console.WriteLine($"{dat:s}");
14+
}
15+
}
16+
// Output with the Heisei era current:
17+
// 1990-01-02T00:00:00
18+
// 1990-01-02T00:00:00
19+
// Output with the new era current:
20+
// 2020-01-02T00:00:00
21+
// 2020-01-02T00:00:00
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>
7+
When eras change, calling a date and time instantiation method that relies on the default era can produce an ambiguous date.
8+
In the example below, the call to the <code>JapaneseCalendar.ToDateTime</code> method that uses the default era returns different dates depending on whether or not the new era has been defined in the registry.
9+
</p>
10+
</overview>
11+
<recommendation>
12+
<p>Use speific era when creating DateTime and DateTimeOffset structs from previously stored date in Japanese calendar</p>
13+
<p>Don't store dates in Japanese format</p>
14+
<p>Don't use hard-coded era start date for date calculations converting dates from Japanese date format</p>
15+
<p>Use <code>JapaneseCalendar</code> class for date formatting only</p>
16+
</recommendation>
17+
<example>
18+
<p>This example demonstrates the dangers of using current year assumptions in Japanese date conversions</p>
19+
<sample src="MishandlingJapaneseEra.cs" />
20+
</example>
21+
22+
<references>
23+
<li>
24+
<a href="https://devblogs.microsoft.com/dotnet/handling-a-new-era-in-the-japanese-calendar-in-net/">Handling a new era in the Japanese calendar in .NET</a>.
25+
</li>
26+
<li>
27+
<a href="https://blogs.msdn.microsoft.com/shawnste/2018/04/12/the-japanese-calendars-y2k-moment/">The Japanese Calendar's Y2K Moment</a>.
28+
</li>
29+
<li>
30+
<a href="https://docs.microsoft.com/en-us/windows/desktop/Intl/era-handling-for-the-japanese-calendar/">Era Handling for the Japanese Calendar</a>.
31+
</li>
32+
<li>
33+
<a href="https://simple.wikipedia.org/wiki/List_of_Japanese_eras">List of Japanese Eras (Wikipedia)</a>
34+
</li>
35+
</references>
36+
</qhelp>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* @name Mishandling the Japanese era start date
3+
* @description Japanese era should be handled with the built-in 'JapaneseCalendar' class. Avoid hard-coding Japanese era start dates and names.
4+
* @id cs/mishandling-japanese-era
5+
* @kind problem
6+
* @problem.severity warning
7+
* @precision medium
8+
* @tags reliability
9+
* date-time
10+
*/
11+
12+
import csharp
13+
14+
/**
15+
* Holds if `year`, `month`, and `day` specify the start of a new era
16+
* (see https://simple.wikipedia.org/wiki/List_of_Japanese_eras).
17+
*/
18+
predicate isEraStart(int year, int month, int day) {
19+
year = 1989 and month = 1 and day = 8
20+
or
21+
year = 2019 and month = 5 and day = 1
22+
}
23+
24+
predicate isExactEraStartDateCreation(ObjectCreation cr) {
25+
(
26+
cr.getType().hasQualifiedName("System.DateTime") or
27+
cr.getType().hasQualifiedName("System.DateTimeOffset")
28+
) and
29+
isEraStart(cr.getArgument(0).getValue().toInt(), cr.getArgument(1).getValue().toInt(), cr.getArgument(2).getValue().toInt())
30+
}
31+
32+
predicate isDateFromJapaneseCalendarToDateTime(MethodCall mc) {
33+
(
34+
mc.getQualifier().getType().hasQualifiedName("System.Globalization.JapaneseCalendar") or
35+
mc.getQualifier().getType().hasQualifiedName("System.Globalization.JapaneseLunisolarCalendar")
36+
) and
37+
mc.getTarget().hasName("ToDateTime") and
38+
mc.getArgument(0).hasValue() and
39+
(
40+
mc.getNumberOfArguments() = 7 // implicitly current era
41+
or
42+
mc.getNumberOfArguments() = 8 and
43+
mc.getArgument(7).getValue() = "0"
44+
) // explicitly current era
45+
}
46+
47+
predicate isDateFromJapaneseCalendarCreation(ObjectCreation cr) {
48+
(
49+
cr.getType().hasQualifiedName("System.DateTime") or
50+
cr.getType().hasQualifiedName("System.DateTimeOffset")
51+
) and
52+
(
53+
cr
54+
.getArgumentForName("calendar")
55+
.getType()
56+
.hasQualifiedName("System.Globalization.JapaneseCalendar") or
57+
cr
58+
.getArgumentForName("calendar")
59+
.getType()
60+
.hasQualifiedName("System.Globalization.JapaneseLunisolarCalendar")
61+
) and
62+
cr.getArgumentForName("year").hasValue()
63+
}
64+
65+
from Expr expr, string message
66+
where
67+
isDateFromJapaneseCalendarToDateTime(expr) and message = "'DateTime' created from Japanese calendar with explicit or current era and hard-coded year."
68+
or
69+
isDateFromJapaneseCalendarCreation(expr) and message = "'DateTime' constructed from Japanese calendar with explicit or current era and hard-coded year."
70+
or
71+
isExactEraStartDateCreation(expr) and message = "Hard-coded the beginning of the Japanese Heisei era."
72+
select expr, message
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
| Program.cs:12:31:12:54 | object creation of type DateTime | Hard-coded the beginning of the Japanese Heisei era. |
2+
| Program.cs:15:66:15:89 | object creation of type DateTime | Hard-coded the beginning of the Japanese Heisei era. |
3+
| Program.cs:22:42:22:98 | object creation of type DateTimeOffset | Hard-coded the beginning of the Japanese Heisei era. |
4+
| Program.cs:29:37:29:71 | call to method ToDateTime | 'DateTime' created from Japanese calendar with explicit or current era and hard-coded year. |
5+
| Program.cs:33:27:33:64 | call to method ToDateTime | 'DateTime' created from Japanese calendar with explicit or current era and hard-coded year. |
6+
| Program.cs:49:28:49:73 | object creation of type DateTime | 'DateTime' constructed from Japanese calendar with explicit or current era and hard-coded year. |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Likely Bugs/MishandlingJapaneseEra.ql
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Globalization;
4+
5+
namespace JapaneseDates
6+
{
7+
class Program
8+
{
9+
static void Main(string[] args)
10+
{
11+
// BAD: hard-coded era start date
12+
var henseiStart = new DateTime(1989, 1, 8);
13+
14+
// BAD: hard-coded era start dates, list
15+
List<DateTime> listOfEraStart = new List<DateTime> { new DateTime(1989, 1, 8) };
16+
17+
// BAD: hardcoded era name
18+
string currentEra = "Heisei";
19+
20+
DateTimeOffset dateNow = DateTimeOffset.Now;
21+
22+
DateTimeOffset dateThisEra = new DateTimeOffset(1989, 1, 8, 0, 0, 0, 0, TimeSpan.Zero);
23+
24+
CultureInfo japaneseCulture = CultureInfo.GetCultureInfo("ja-JP");
25+
26+
JapaneseCalendar jk = new JapaneseCalendar();
27+
28+
// BAD: datetime is created from constant year in the current era, and the result will change with era change
29+
var datejkCurrentEra = jk.ToDateTime(32, 2, 1, 9, 9, 9, 9);
30+
Console.WriteLine("Date for datejkCurrentEra {0} and year {1}", datejkCurrentEra.ToString(japaneseCulture), jk.GetYear (datejkCurrentEra));
31+
32+
// BAD: datetime is created from constant year in the current era, and the result will change with era change
33+
var datejk = jk.ToDateTime(32, 2, 1, 9, 9, 9, 9, 0);
34+
Console.WriteLine("Date for jk {0} and year {1}", datejk.ToString(japaneseCulture), jk.GetYear (datejk));
35+
36+
// OK: datetime is created from constant year in the specific era, and the result will not change with era change
37+
var datejk1 = jk.ToDateTime(32, 2, 1, 9, 9, 9, 9, 4);
38+
Console.WriteLine("Date for jk {0} and year {1}", datejk1.ToString(japaneseCulture), jk.GetYear (datejk1));
39+
40+
// OK: year is not hard-coded, i.e. it may be updated
41+
var datejk0 = jk.ToDateTime(jk.GetYear(datejk), 2, 1, 9, 9, 9, 9);
42+
Console.WriteLine("Date for jk0 {0} and year {1}", datejk0, jk.GetYear(datejk0));
43+
44+
// BAD: hard-coded year conversion
45+
int realYear = 1988 + jk.GetYear(datejk);
46+
Console.WriteLine("Which converts to year {0}", realYear);
47+
48+
// BAD: creating DateTime using specified Japanese era date. This may yield a different date when era changes
49+
DateTime val = new DateTime(32, 2, 1, new JapaneseCalendar());
50+
Console.WriteLine("DateTime from constructor {0}", val);
51+
52+
// OK: variable data for Year, not necessarily hard-coded and can come from adjusted source
53+
DateTime val1 = new DateTime(jk.GetYear(datejk), 2, 1, new JapaneseCalendar());
54+
Console.WriteLine("DateTime from constructor {0}", val);
55+
}
56+
}
57+
}

0 commit comments

Comments
 (0)