Skip to content

Commit a862405

Browse files
committed
New ToString() options.
* Splitting the new HTML escaping functionality into ToHtmlEscapedString(). * A new ToString() overload that accepts a JsonSerializerSettings instance.
1 parent b515712 commit a862405

File tree

7 files changed

+119
-15
lines changed

7 files changed

+119
-15
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: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,22 @@ public partial class Thing : JsonLdObject
1010
private const string ContextPropertyJson = "\"@context\":\"http://schema.org\",";
1111

1212
/// <summary>
13-
/// Serializer settings used.
14-
/// Note: Escapes HTML to avoid XSS vulnerabilities where user-supplied data is used.
13+
/// Default serializer settings used.
1514
/// </summary>
1615
private static readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings()
16+
{
17+
Converters = new List<JsonConverter>()
18+
{
19+
new StringEnumConverter()
20+
},
21+
NullValueHandling = NullValueHandling.Ignore
22+
};
23+
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()
1729
{
1830
Converters = new List<JsonConverter>()
1931
{
@@ -29,8 +41,34 @@ public partial class Thing : JsonLdObject
2941
/// <returns>
3042
/// A <see cref="string" /> that represents the JSON-LD representation of this instance.
3143
/// </returns>
32-
public override string ToString() =>
33-
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));
3472

3573
private static string RemoveAllButFirstContext(string json)
3674
{

Tests/Schema.NET.Test/ProductTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public class ProductTest
4444
"\"@context\":\"http://schema.org\"," +
4545
"\"@type\":\"Product\"," +
4646
"\"name\":\"Executive Anvil\"," +
47-
"\"description\":\"Sleeker than ACME\\u0027s Classic Anvil, the Executive Anvil is perfect for the business traveller looking for something to drop from a height.\"," +
47+
"\"description\":\"Sleeker than ACME's Classic Anvil, the Executive Anvil is perfect for the business traveller looking for something to drop from a height.\"," +
4848
"\"image\":\"http://www.example.com/anvil_executive.jpg\"," +
4949
"\"aggregateRating\":{" +
5050
"\"@type\":\"AggregateRating\"," +

Tests/Schema.NET.Test/RecipeTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ public class RecipeTest
4545
"{" +
4646
"\"@context\":\"http://schema.org\"," +
4747
"\"@type\":\"Recipe\"," +
48-
"\"name\":\"Grandma\\u0027s Holiday Apple Pie\"," +
49-
"\"description\":\"This is my grandmother\\u0027s apple pie recipe. I like to add a dash of nutmeg.\"," +
48+
"\"name\":\"Grandma's Holiday Apple Pie\"," +
49+
"\"description\":\"This is my grandmother's apple pie recipe. I like to add a dash of nutmeg.\"," +
5050
"\"image\":\"https://example.com/image.jpg\"," +
5151
"\"aggregateRating\":{" +
5252
"\"@type\":\"AggregateRating\"," +

Tests/Schema.NET.Test/RestaurantTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public class RestaurantTest
6666
"\"@context\":\"http://schema.org\"," +
6767
"\"@type\":\"Restaurant\"," +
6868
"\"@id\":\"http://davessteakhouse.example.com\"," +
69-
"\"name\":\"Dave\\u0027s Steak House\"," +
69+
"\"name\":\"Dave's Steak House\"," +
7070
"\"image\":\"http://davessteakhouse.example.com/logo.jpg\"," +
7171
"\"sameAs\":\"http://davessteakhouse.example.com\"," +
7272
"\"address\":{" +
Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,40 @@
11
namespace Schema.NET.Test
22
{
33
using System;
4+
using System.Collections.Generic;
5+
using Newtonsoft.Json;
6+
using Newtonsoft.Json.Converters;
47
using Xunit;
58

69
// https://developers.google.com/search/docs/data-types/books
710
public class SerializationTest
811
{
12+
private static readonly Person Person = new Person()
13+
{
14+
Name = "J.D. Salinger</script><script>alert('gotcha');</script>",
15+
Description = null
16+
};
17+
918
private readonly Book book = new Book()
1019
{
1120
Id = new Uri("http://example.com/book/1"),
1221
Name = "The Catcher in the Rye</script><script>alert('gotcha');</script>",
13-
Author = new Person()
14-
{
15-
Name = "J.D. Salinger</script><script>alert('gotcha');</script>"
16-
}
22+
Author = Person
1723
};
1824

1925
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 =
2038
"{" +
2139
"\"@context\":\"http://schema.org\"," +
2240
"\"@type\":\"Book\"," +
@@ -28,8 +46,43 @@ public class SerializationTest
2846
"}" +
2947
"}";
3048

49+
private readonly JsonSerializerSettings customSerializerSettings = new JsonSerializerSettings()
50+
{
51+
Converters = new List<JsonConverter>()
52+
{
53+
new StringEnumConverter()
54+
},
55+
NullValueHandling = NullValueHandling.Include, // changed
56+
StringEscapeHandling = StringEscapeHandling.EscapeHtml,
57+
};
58+
59+
private readonly string jsonCustom =
60+
"{" +
61+
"\"@context\":\"http://schema.org\"," +
62+
"\"@type\":\"Person\"," +
63+
"\"@id\":null," +
64+
"\"name\":\"J.D. Salinger\\u003c/script\\u003e\\u003cscript\\u003ealert(\\u0027gotcha\\u0027);\\u003c/script\\u003e\"," +
65+
"\"description\":null,\"additionalType\":null,\"alternateName\":null,\"disambiguatingDescription\":null,\"identifier\":null," +
66+
"\"image\":null,\"mainEntityOfPage\":null,\"potentialAction\":null,\"sameAs\":null,\"url\":null,\"additionalName\":null," +
67+
"\"address\":null,\"affiliation\":null,\"alumniOf\":null,\"award\":null,\"birthDate\":null,\"birthPlace\":null,\"brand\":null," +
68+
"\"children\":null,\"colleague\":null,\"contactPoint\":null,\"deathDate\":null,\"deathPlace\":null,\"duns\":null,\"email\":null," +
69+
"\"familyName\":null,\"faxNumber\":null,\"follows\":null,\"funder\":null,\"gender\":null,\"givenName\":null," +
70+
"\"globalLocationNumber\":null,\"hasOfferCatalog\":null,\"hasPOS\":null,\"height\":null,\"homeLocation\":null," +
71+
"\"honorificPrefix\":null,\"honorificSuffix\":null,\"isicV4\":null,\"jobTitle\":null,\"knows\":null,\"makesOffer\":null," +
72+
"\"memberOf\":null,\"naics\":null,\"nationality\":null,\"netWorth\":null,\"owns\":null,\"parent\":null,\"performerIn\":null," +
73+
"\"publishingPrinciples\":null,\"relatedTo\":null,\"seeks\":null,\"sibling\":null,\"sponsor\":null,\"spouse\":null,\"taxID\":null," +
74+
"\"telephone\":null,\"vatID\":null,\"weight\":null,\"workLocation\":null,\"worksFor\":null}";
75+
3176
[Fact]
3277
public void ToString_UnsafeBookData_ReturnsExpectedJsonLd() =>
3378
Assert.Equal(this.json, this.book.ToString());
79+
80+
[Fact]
81+
public void ToHtmlEscapedString_UnsafeBookData_ReturnsExpectedJsonLd() =>
82+
Assert.Equal(this.jsonHtmlEscaped, this.book.ToHtmlEscapedString());
83+
84+
[Fact]
85+
public void ToStringWithCustomSerializerSettings_UnsafeAuthorData_ReturnsExpectedJsonLd() =>
86+
Assert.Equal(this.jsonCustom, Person.ToString(this.customSerializerSettings));
3487
}
3588
}

Tests/Schema.NET.Test/WebsiteTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public void ToString_SiteLinksSearchBoxGoogleStructuredData_ReturnsExpectedJsonL
2525
"\"@type\":\"WebSite\"," +
2626
"\"potentialAction\":{" +
2727
"\"@type\":\"SearchAction\"," +
28-
"\"target\":\"http://example.com/search?\\u0026q={query}\"," +
28+
"\"target\":\"http://example.com/search?&q={query}\"," +
2929
"\"query-input\":\"required\"" +
3030
"}," +
3131
"\"url\":\"https://example.com\"" +

0 commit comments

Comments
 (0)