Skip to content

Commit eacded2

Browse files
author
Denis Levin
committed
Japanese Era and Leap Year checks (Likely Bugs)
1 parent 29ae7b5 commit eacded2

14 files changed

Lines changed: 364 additions & 0 deletions
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<include src="LeapYear.qhelp" />
7+
8+
<p>When creating or creating a <code>System.DateTime</code> object by setting the year, month, day in the contructor (other parameters optional) by performing an arithmetic operation on a different <code>DateTime</code> object, there is a risk that the date you are setting is an invalid one.</p>
9+
<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 un-representable DateTime."</code></p>
10+
11+
</overview>
12+
<recommendation>
13+
<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>
14+
15+
</recommendation>
16+
<example>
17+
<p>In this example, we are adding/decreasing 1 year to the current date 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>
18+
<sample src="UnsafeYearConstructionBad.cs" />
19+
20+
<p>To fix this bug, we add/substract years to the current date by calling <code>AddYears</code> method on it.</p>
21+
<sample src="UnsafeYearConstructionGood.cs" />
22+
</example>
23+
24+
</qhelp>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* @name Unsafe year argument for DateTime constructor
3+
* @description Constructing a DateTime object by setting the year argument by manipulating the year of a different DateTime object
4+
* @kind problem
5+
* @problem.severity error
6+
* @id cs/leap-year/unsafe-year-contruction
7+
* @precision high
8+
* @tags security
9+
* leap-year
10+
*/
11+
12+
import csharp
13+
import semmle.code.csharp.dataflow.TaintTracking
14+
15+
class UnsafeYearCreationFromArithmeticConfiguration extends TaintTracking::Configuration {
16+
UnsafeYearCreationFromArithmeticConfiguration() { this = "UnsafeYearCreationFromArithmeticConfiguration" }
17+
18+
override predicate isSource(DataFlow::Node source) {
19+
exists( ArithmeticOperation ao, PropertyAccess pa |
20+
ao = source.asExpr() |
21+
pa = ao.getAChild*()
22+
and pa.getProperty().getQualifiedName().matches("%DateTime.Year")
23+
)
24+
}
25+
26+
override predicate isSink(DataFlow::Node sink) {
27+
exists( ObjectCreation oc |
28+
sink.asExpr() = oc.getArgumentForName("year")
29+
and oc.getObjectType().getABaseType*().hasQualifiedName("System.DateTime"))
30+
}
31+
}
32+
33+
from UnsafeYearCreationFromArithmeticConfiguration config, Expr sink, Expr source
34+
where config.hasFlow(DataFlow::exprNode(source), DataFlow::exprNode(sink))
35+
select 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: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
3+
public class UnsafeYearConstructionBad
4+
{
5+
public UnsafeYearConstructionBad()
6+
{
7+
DateTime Start;
8+
DateTime End;
9+
var now = DateTime.UtcNow;
10+
11+
// the base-date +/1 n years may not be a valid date.
12+
Start = new DateTime(now.Year - 1, now.Month, now.Day, 0, 0, 0, DateTimeKind.Utc);
13+
End = new DateTime(now.Year + 1, now.Month, now.Day, 0, 0, 1, DateTimeKind.Utc);
14+
}
15+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System;
2+
3+
public class UnsafeYearConstructionBad
4+
{
5+
public UnsafeYearConstructionBad()
6+
{
7+
DateTime Start;
8+
DateTime End;
9+
var now = DateTime.UtcNow;
10+
11+
Start = now.AddYears(-1).Date;
12+
End = now.AddYears(-1).Date.AddSeconds(1);
13+
}
14+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System;
2+
3+
using System.Globalization;
4+
5+
public class Example
6+
7+
{
8+
9+
public static void Main()
10+
11+
{
12+
13+
var cal = new JapaneseCalendar();
14+
15+
// constructing date using current era
16+
var dat = cal.ToDateTime(2, 1, 2, 0, 0, 0, 0);
17+
18+
Console.WriteLine($"{dat:s}");
19+
20+
// constructing date using current era
21+
dat = new DateTime(2, 1, 2, cal);
22+
23+
Console.WriteLine($"{dat:s}");
24+
25+
}
26+
27+
}
28+
29+
// Output with the Heisei era current:
30+
31+
// 1990-01-02T00:00:00
32+
33+
// 1990-01-02T00:00:00
34+
35+
// Output with the new era current:
36+
37+
// 2020-01-02T00:00:00
38+
39+
// 2020-01-02T00:00:00
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
</references>
33+
</qhelp>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* @name MishandlingJapaneseEraStartDate
3+
* @description Japanese Era should be handled with built-in JapaneseCalendar class. Aviod 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+
* japanese-era
10+
*/
11+
12+
import csharp
13+
14+
predicate isExactEraStartDateCreation(ObjectCreation cr) {
15+
(cr.getType().hasQualifiedName("System.DateTime") or cr.getType().hasQualifiedName("System.DateTimeOffset"))
16+
and
17+
cr.getArgument(0).getValue() = "1989" and
18+
cr.getArgument(1).getValue() = "1" and
19+
cr.getArgument(2).getValue() = "8"
20+
}
21+
22+
predicate isDateFromJapaneseCalendarToDateTime(MethodCall mc) {
23+
(mc.getQualifier().getType().hasQualifiedName("System.Globalization.JapaneseCalendar") or mc.getQualifier().getType().hasQualifiedName("System.Globalization.JapaneseLunisolarCalendar"))
24+
and
25+
mc.getTarget().hasName("ToDateTime") and
26+
mc.getArgument(0).getValue() != "" and
27+
(mc.getNumberOfArguments() = 7 or // implicitly current era
28+
mc.getNumberOfArguments() = 8 and
29+
mc.getArgument(7).getValue() = "0") // explicitly current era
30+
}
31+
32+
predicate isDateFromJapaneseCalendarCreation(ObjectCreation cr) {
33+
(cr.getType().hasQualifiedName("System.DateTime") or cr.getType().hasQualifiedName("System.DateTimeOffset")) and
34+
(cr.getArgumentForName("calendar").getType().hasQualifiedName("System.Globalization.JapaneseCalendar") or cr.getArgumentForName("calendar").getType().hasQualifiedName("System.Globalization.JapaneseLunisolarCalendar"))
35+
and
36+
cr.getArgumentForName("year").getValue() != ""
37+
}
38+
39+
predicate inEraArrayCreation(ArrayInitializer ai) {
40+
ai.getElement(0).getValue() = "1867" and
41+
ai.getElement(1).getValue() = "1911" and
42+
ai.getElement(2).getValue() = "1925" and
43+
ai.getElement(3).getValue() = "1988"
44+
}
45+
46+
predicate isEraCollectionCreation(CollectionInitializer cs) {
47+
cs.getElementInitializer(0).getValue() = "1867" and
48+
cs.getElementInitializer(1).getValue() = "1911" and
49+
cs.getElementInitializer(2).getValue() = "1925" and
50+
cs.getElementInitializer(3).getValue() = "1988"
51+
}
52+
53+
from Expr expr, string message
54+
where
55+
isDateFromJapaneseCalendarToDateTime(expr) and message = "DateTime created from Japanese calendar with explicit or current era and hard-coded year" or
56+
isDateFromJapaneseCalendarCreation(expr) and message = "DateTime constructed from Japanese calendar with explicit or current era and hard-coded year" or
57+
isEraCollectionCreation(expr) and message = "Hard-coded collection with Japanese era years" or
58+
inEraArrayCreation (expr) and message = "Hard-coded array with Japanese era years" or
59+
isExactEraStartDateCreation(expr) and message = "Hard-coded the beginning of the Japanese Heisei era"
60+
select expr, message
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
| Program.cs:13:39:13:50 | ... - ... | This $@ based on a System.DateTime.Year property is used in a construction of a new System.DateTime object, flowing to the 'year' argument. | Program.cs:13:39:13:50 | ... - ... | arithmetic operation |
2+
| Program.cs:17:37:17:43 | access to local variable endYear | This $@ based on a System.DateTime.Year property is used in a construction of a new System.DateTime object, flowing to the 'year' argument. | Program.cs:15:27:15:38 | ... + ... | arithmetic operation |
3+
| Program.cs:26:39:26:42 | access to parameter year | This $@ based on a System.DateTime.Year property is used in a construction of a new System.DateTime object, flowing to the 'year' argument. | Program.cs:33:18:33:29 | ... - ... | arithmetic operation |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Likely Bugs/LeapYear/UnsafeYearConstruction.ql
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System;
2+
3+
namespace LeapYear
4+
{
5+
public class PipelineProperties
6+
{
7+
public DateTime Start;
8+
public DateTime End;
9+
public PipelineProperties()
10+
{
11+
var now = DateTime.UtcNow;
12+
// BUG
13+
this.Start = new DateTime(now.Year - 1, now.Month, now.Day, 0, 0, 0, DateTimeKind.Utc);
14+
15+
var endYear = now.Year + 1;
16+
// BUG
17+
this.End = new DateTime(endYear, now.Month, now.Day, 0, 0, 1, DateTimeKind.Utc);
18+
19+
// OK
20+
this.Start = now.AddYears(-1).Date;
21+
}
22+
23+
private void Test(int year, int month, int day)
24+
{
25+
// BUG (arithmetic operation from StartTest)
26+
this.Start = new DateTime(year, month, day);
27+
}
28+
29+
public void StartTest()
30+
{
31+
var now = DateTime.UtcNow;
32+
// flows into Test (source for bug)
33+
Test(now.Year - 1, now.Month, now.Day);
34+
}
35+
36+
public void StartTestFP()
37+
{
38+
var now = DateTime.UtcNow;
39+
Test(1900 + 80, now.Month, now.Day);
40+
}
41+
}
42+
}

0 commit comments

Comments
 (0)