Skip to content

Commit 703e6b3

Browse files
authored
Merge pull request #19 from benmccallum/patch-1
Avoid XSS by escaping HTML during serialization
2 parents 94670d7 + 2d57b83 commit 703e6b3

File tree

4 files changed

+161
-7
lines changed

4 files changed

+161
-7
lines changed

README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
![Schema.NET Banner](https://raw.githubusercontent.com/RehanSaeed/Schema.NET/master/Images/Banner.png)
1+
![Schema.NET Banner](https://raw.githubusercontent.com/RehanSaeed/Schema.NET/master/Images/Banner.png)
22

33
Schema.org objects turned into strongly typed C# POCO classes for use in .NET. All classes can be serialized into JSON/JSON-LD and XML, typically used to represent structured data in the `head` section of `html` page.
44

@@ -16,7 +16,7 @@ var jsonLd = website.ToString();
1616

1717
The code above outputs the following JSON-LD:
1818

19-
```JSONLD
19+
```JSON
2020
{
2121
"@context":"http://schema.org",
2222
"@type":"WebSite",
@@ -26,6 +26,8 @@ The code above outputs the following JSON-LD:
2626
}
2727
```
2828

29+
If writing the result into a `<script>` element, be sure to use the `.ToHtmlEscapedString()` method instead to avoid exposing your website to a Cross-Site Scripting attack. See the [example below](#important-security-notice).
30+
2931
## What is Schema.org?
3032

3133
[schema.org](https://schema.org) defines a set of standard classes and their properties for objects and services in the real world. This machine readable format is a common standard used across the web for describing things.
@@ -56,6 +58,17 @@ Using structured data in `html` requires the use of a `script` tag with a MIME t
5658
</script>
5759
```
5860

61+
##### Important Security Notice
62+
When serializing the result for a website's `<script>` tag, you should use the alternate `.ToHtmlEscapedString()` to avoid exposing yourself to a Cross-Site Scripting (XSS) vulnerability if some of the properties in your schema have been set from untrusted sources.
63+
Usage in an ASP.NET MVC project might look like this:
64+
65+
```HTML
66+
<script type="application/ld+json">
67+
@Html.Raw(Model.Schema.ToHtmlEscapedString())
68+
</script>
69+
```
70+
71+
5972
#### Windows UWP Sharing
6073

6174
Windows UWP apps let you share data using schema.org classes. [Here](https://docs.microsoft.com/en-us/uwp/schemas/appxpackage/appxmanifestschema/element-sharetarget) is an example showing how to share metadata about a book.

Source/Schema.NET/Thing.Partial.cs

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace Schema.NET
1+
namespace Schema.NET
22
{
33
using System.Collections.Generic;
44
using System.Text;
@@ -8,6 +8,10 @@
88
public partial class Thing : JsonLdObject
99
{
1010
private const string ContextPropertyJson = "\"@context\":\"http://schema.org\",";
11+
12+
/// <summary>
13+
/// Default serializer settings used.
14+
/// </summary>
1115
private static readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings()
1216
{
1317
Converters = new List<JsonConverter>()
@@ -17,14 +21,54 @@ public partial class Thing : JsonLdObject
1721
NullValueHandling = NullValueHandling.Ignore
1822
};
1923

24+
/// <summary>
25+
/// Serializer settings used when trying to avoid XSS vulnerabilities where user-supplied data is used
26+
/// and the output of the serialization is embedded into a web page raw.
27+
/// </summary>
28+
private static readonly JsonSerializerSettings HtmlEscapedSerializerSettings = new JsonSerializerSettings()
29+
{
30+
Converters = new List<JsonConverter>()
31+
{
32+
new StringEnumConverter()
33+
},
34+
NullValueHandling = NullValueHandling.Ignore,
35+
StringEscapeHandling = StringEscapeHandling.EscapeHtml
36+
};
37+
2038
/// <summary>
2139
/// Returns the JSON-LD representation of this instance.
2240
/// </summary>
2341
/// <returns>
2442
/// A <see cref="string" /> that represents the JSON-LD representation of this instance.
2543
/// </returns>
26-
public override string ToString() =>
27-
RemoveAllButFirstContext(JsonConvert.SerializeObject(this, SerializerSettings));
44+
public override string ToString() => this.ToString(SerializerSettings);
45+
46+
/// <summary>
47+
/// Returns the JSON-LD representation of this instance.
48+
///
49+
/// This method should be used when you want to embed the output raw (as-is) into a web
50+
/// page. It uses serializer settings with HTML escaping to avoid Cross-Site Scripting (XSS)
51+
/// vulnerabilities if the object was constructed from an untrusted source.
52+
/// </summary>
53+
/// <returns>
54+
/// A <see cref="string" /> that represents the JSON-LD representation of this instance.
55+
/// </returns>
56+
public string ToHtmlEscapedString() => this.ToString(HtmlEscapedSerializerSettings);
57+
58+
/// <summary>
59+
/// Returns the JSON-LD representation of this instance using the <see cref="JsonSerializerSettings"/> provided.
60+
///
61+
/// Caution: You should ensure your <paramref name="serializerSettings"/> has
62+
/// <see cref="JsonSerializerSettings.StringEscapeHandling"/> set to <see cref="StringEscapeHandling.EscapeHtml"/>
63+
/// if you plan to embed the output using @Html.Raw anywhere in a web page, else you open yourself up a possible
64+
/// Cross-Site Scripting (XSS) attack if untrusted data is set on any of this object's properties.
65+
/// </summary>
66+
/// <param name="serializerSettings">Serialization settings.</param>
67+
/// <returns>
68+
/// A <see cref="string" /> that represents the JSON-LD representation of this instance.
69+
/// </returns>
70+
public string ToString(JsonSerializerSettings serializerSettings) =>
71+
RemoveAllButFirstContext(JsonConvert.SerializeObject(this, serializerSettings));
2872

2973
private static string RemoveAllButFirstContext(string json)
3074
{

Tests/Schema.NET.Test/JobPostingTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public class JobPostingTest
1010
private readonly JobPosting jobPosting = new JobPosting()
1111
{
1212
Title = "Fitter and Turner", // Required
13-
Description = "<p>Widget assembly role for pressing wheel assemblies.</p><p><strong>Educational Requirements:</strong> Completed level 2 ISTA Machinist Apprenticeship.</p><p><strong>Required Experience:</strong> At least 3 years in a machinist role.</p>", // Required
13+
Description = "Widget assembly role for pressing wheel assemblies. Educational Requirements: Completed level 2 ISTA Machinist Apprenticeship. Required Experience: At least 3 years in a machinist role.", // Required
1414
Identifier = new PropertyValue() // Recommended
1515
{
1616
Name = "MagsRUs Wheel Company",
@@ -51,7 +51,7 @@ public class JobPostingTest
5151
"\"@context\":\"http://schema.org\"," +
5252
"\"@type\":\"JobPosting\"," +
5353
"\"title\":\"Fitter and Turner\"," +
54-
"\"description\":\"<p>Widget assembly role for pressing wheel assemblies.</p><p><strong>Educational Requirements:</strong> Completed level 2 ISTA Machinist Apprenticeship.</p><p><strong>Required Experience:</strong> At least 3 years in a machinist role.</p>\"," +
54+
"\"description\":\"Widget assembly role for pressing wheel assemblies. Educational Requirements: Completed level 2 ISTA Machinist Apprenticeship. Required Experience: At least 3 years in a machinist role.\"," +
5555
"\"identifier\":{" +
5656
"\"@type\":\"PropertyValue\"," +
5757
"\"name\":\"MagsRUs Wheel Company\"," +
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
namespace Schema.NET.Test
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
using Newtonsoft.Json;
6+
using Newtonsoft.Json.Converters;
7+
using Xunit;
8+
9+
// https://developers.google.com/search/docs/data-types/books
10+
public class SerializationTest
11+
{
12+
private static readonly Person Person = new Person()
13+
{
14+
Name = "J.D. Salinger</script><script>alert('gotcha');</script>",
15+
Description = null
16+
};
17+
18+
private readonly Book book = new Book()
19+
{
20+
Id = new Uri("http://example.com/book/1"),
21+
Name = "The Catcher in the Rye</script><script>alert('gotcha');</script>",
22+
Author = Person
23+
};
24+
25+
private readonly string json =
26+
"{" +
27+
"\"@context\":\"http://schema.org\"," +
28+
"\"@type\":\"Book\"," +
29+
"\"@id\":\"http://example.com/book/1\"," +
30+
"\"name\":\"The Catcher in the Rye</script><script>alert('gotcha');</script>\"," +
31+
"\"author\":{" +
32+
"\"@type\":\"Person\"," +
33+
"\"name\":\"J.D. Salinger</script><script>alert('gotcha');</script>\"" +
34+
"}" +
35+
"}";
36+
37+
private readonly string jsonHtmlEscaped =
38+
"{" +
39+
"\"@context\":\"http://schema.org\"," +
40+
"\"@type\":\"Book\"," +
41+
"\"@id\":\"http://example.com/book/1\"," +
42+
"\"name\":\"The Catcher in the Rye\\u003c/script\\u003e\\u003cscript\\u003ealert(\\u0027gotcha\\u0027);\\u003c/script\\u003e\"," +
43+
"\"author\":{" +
44+
"\"@type\":\"Person\"," +
45+
"\"name\":\"J.D. Salinger\\u003c/script\\u003e\\u003cscript\\u003ealert(\\u0027gotcha\\u0027);\\u003c/script\\u003e\"" +
46+
"}" +
47+
"}";
48+
49+
private readonly JsonSerializerSettings customSerializerSettings = new JsonSerializerSettings()
50+
{
51+
Converters = new List<JsonConverter>()
52+
{
53+
new StringEnumConverter()
54+
},
55+
NullValueHandling = NullValueHandling.Ignore,
56+
StringEscapeHandling = StringEscapeHandling.EscapeHtml,
57+
Formatting = Formatting.Indented
58+
};
59+
60+
private readonly string jsonCustom =
61+
"{\r\n" +
62+
" \"@context\": \"http://schema.org\",\r\n" +
63+
" \"@type\": \"Person\",\r\n" +
64+
" \"name\": \"J.D. Salinger\\u003c/script\\u003e\\u003cscript\\u003ealert(\\u0027gotcha\\u0027);\\u003c/script\\u003e\"\r\n" +
65+
"}";
66+
67+
[Fact]
68+
public void ToString_UnsafeBookData_ReturnsExpectedJsonLd() =>
69+
Assert.Equal(this.json, this.book.ToString());
70+
71+
[Fact]
72+
public void ToHtmlEscapedString_UnsafeBookData_ReturnsExpectedJsonLd() =>
73+
Assert.Equal(this.jsonHtmlEscaped, this.book.ToHtmlEscapedString());
74+
75+
[Fact]
76+
public void ToStringWithCustomSerializerSettings_UnsafeAuthorData_ReturnsExpectedJsonLd() =>
77+
Assert.Equal(this.jsonCustom, Person.ToString(this.customSerializerSettings));
78+
79+
[Fact]
80+
public void ToStringWithNullAssignedProperty_ReturnsExpectedJsonLd()
81+
{
82+
var localBusiness = new LocalBusiness()
83+
{
84+
PriceRange = "$$$",
85+
Address = null
86+
};
87+
var actual = localBusiness.ToString();
88+
var expected =
89+
"{" +
90+
"\"@context\":\"http://schema.org\"," +
91+
"\"@type\":\"LocalBusiness\"," +
92+
"\"priceRange\":\"$$$\"" +
93+
"}";
94+
Assert.Equal(expected, actual);
95+
}
96+
}
97+
}

0 commit comments

Comments
 (0)