Skip to content

Commit caeabc3

Browse files
Merge pull request #64 from aspnet/55_Xml-Escape-Values
Add xml escaping for Expand mode.
2 parents 4820fff + 44c0dd0 commit caeabc3

6 files changed

Lines changed: 39 additions & 11 deletions

File tree

README.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ key name before being inserted into AppSettings. `stripPrefix` is a boolean valu
6969
This setting is a boolean that specified whether to avoid throwing exceptions when the backing configuration source cannot be found or connected.
7070
The default default value is `true`, though some config builders (such as the Azure-based builders) will use a different default.
7171

72+
#### escapeExpandedValues
73+
.Net configuration is XML-based in it's raw form. While these config builders work on `ConfigurationSection` objects in `Strict` and `Greedy` modes,
74+
when operating in `Expand` mode, tokens in the raw XML input are directly replaced with values. Applications that use `Expand` mode may do so because
75+
they need to inject additional XML rather than just a string value. But for the cases when a simple string replacement is the goal, unescaped XML
76+
characters in replacement values may result in invalid XML. In these cases, simply set the `escapeExpandedValues` attribute to `true` and the
77+
config builder will escape special XML characters before replacing tokens in `Expand` mode. The default value is `false`.
78+
7279
#### tokenPattern
7380
This is a setting that is shared between all KeyValueConfigBuilder-derived builders is `tokenPattern`. When describing the `Expand` behavior of these builders
7481
above, it was mentioned that the raw xml is searched for tokens that look like __`${token}`__. This is done with a regular expression. `@"\$\{(\w+)\}"` to be exact.
@@ -119,7 +126,7 @@ preceded with an '@' symbol.
119126
### EnvironmentConfigBuilder
120127
```xml
121128
<add name="Environment"
122-
[mode|@prefix|@stripPrefix|tokenPattern|@optional=true]
129+
[mode|@prefix|@stripPrefix|tokenPattern|@escapeExpandedValues|@optional=true]
123130
type="Microsoft.Configuration.ConfigurationBuilders.EnvironmentConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.Environment" />
124131
```
125132
This is the most basic of the config builders. It draws its values from Environment, and it does not have any additional configuration options.
@@ -132,7 +139,7 @@ This is the most basic of the config builders. It draws its values from Environm
132139
### UserSecretsConfigBuilder
133140
```xml
134141
<add name="UserSecrets"
135-
[mode|@prefix|@stripPrefix|tokenPattern|@optional=true]
142+
[mode|@prefix|@stripPrefix|tokenPattern|@escapeExpandedValues|@optional=true]
136143
(@userSecretsId="12345678-90AB-CDEF-1234-567890" | @userSecretsFile="~\secrets.file")
137144
type="Microsoft.Configuration.ConfigurationBuilders.UserSecretsConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.UserSecrets" />
138145
```
@@ -168,7 +175,7 @@ and currently exposes the format of the file which, as mentioned above, should b
168175
### AzureAppConfigurationBuilder
169176
```xml
170177
<add name="AzureAppConfig"
171-
[mode|@prefix|@stripPrefix|tokenPattern|@optional=false]
178+
[mode|@prefix|@stripPrefix|tokenPattern|@escapeExpandedValues|@optional=false]
172179
(@vaultName="MyVaultName" | @uri="https://MyVaultName.vault.azure.net")
173180
[@connectionString="connection string"]
174181
[@version="secrets version"]
@@ -211,7 +218,7 @@ entries: `item1` and `item2`.
211218
### AzureKeyVaultConfigBuilder
212219
```xml
213220
<add name="AzureKeyVault"
214-
[mode|@prefix|@stripPrefix|tokenPattern|@optional=false]
221+
[mode|@prefix|@stripPrefix|tokenPattern|@escapeExpandedValues|@optional=false]
215222
(@vaultName="MyVaultName" | @uri="https://MyVaultName.vault.azure.net")
216223
[@connectionString="connection string"]
217224
[@version="secrets version"]
@@ -249,7 +256,7 @@ entries: `item1` and `item2`.
249256
### KeyPerFileConfigBuilder
250257
```xml
251258
<add name="KeyPerFile"
252-
[mode|@prefix|@stripPrefix|tokenPattern|@optional=false]
259+
[mode|@prefix|@stripPrefix|tokenPattern|@escapeExpandedValues|@optional=false]
253260
(@directoryPath="PathToSourceDirectory")
254261
[@ignorePrefix="ignore."]
255262
[keyDelimiter=":"]
@@ -267,7 +274,7 @@ their orchestrated windows containers in this key-per-file manner.
267274
### SimpleJsonConfigBuilder
268275
```xml
269276
<add name="SimpleJson"
270-
[mode|@prefix|@stripPrefix|tokenPattern|@optional=true]
277+
[mode|@prefix|@stripPrefix|tokenPattern|@escapeExpandedValues|@optional=true]
271278
@jsonFile="~\config.json"
272279
[@jsonMode="(Flat|Sectional)"]
273280
type="Microsoft.Configuration.ConfigurationBuilders.SimpleJsonConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.Json" />
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"expandTestCS": "A & really ' bad \" unescaped < connection > string."
3+
}

samples/SampleWebApp/App_Data/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"jsonComplex": {
2222
"setting1": "Complex Setting 1",
2323
"setting2": "Complex Setting 2",
24-
"jsonArrayOfSettings": [ "one", "two", "three"]
24+
"jsonArrayOfSettings": [ "one", "two", "three" ]
2525
}
2626
}
2727
}

samples/SampleWebApp/SampleWebApp.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
</ItemGroup>
9292
<ItemGroup>
9393
<Content Include="App_Data\settings.json" />
94+
<Content Include="App_Data\expandTest.json" />
9495
<None Include="packages.config" />
9596
<None Include="Web.Debug.config">
9697
<DependentUpon>Web.config</DependentUpon>

samples/SampleWebApp/Web.config

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
<add name="KV4" vaultName="${ConfigBuilderTestKeyVaultName}" mode="Greedy" version="0de51928e49144ce86eb1de9056ac937" type="Microsoft.Configuration.ConfigurationBuilders.AzureKeyVaultConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.Azure, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
3434

3535
<add name="AS_Sub_Test" optional="${Boolean}" type="Microsoft.Configuration.ConfigurationBuilders.EnvironmentConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.Environment, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
36-
</builders>
36+
<add name="ExpTest" mode="Expand" escapeExpandedValues="true" jsonFile="~/App_Data/expandTest.json" jsonMode="Flat" type="Microsoft.Configuration.ConfigurationBuilders.SimpleJsonConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.Json, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
37+
</builders>
3738
</configBuilders>
3839

3940
<appSettings configBuilders="Environment,Secrets,Json,KeyPerFile">
@@ -62,7 +63,9 @@
6263
<!-- key="Secret3" value="Will be added by KV3:Latest3. IFF already added, will be "updated" by KV1 to Latest3. KV2 and KV4 don't have versions matching this secret." -->
6364
</appSettings>
6465

65-
<connectionStrings configBuilders="Json">
66+
<connectionStrings configBuilders="Json,ExpTest">
67+
<add name="expansionTest" connectionString="${expandTestCS}" />
68+
<add name="expandTestCS" connectionString="Only replaced in Strict/Greedy modes. Not Expand."/>
6669
<add name="jsonConnectionString1" connectionString="Will be replaced by 'Json' in 'Flat' AND 'Sectional' jsonModes, but with different values." />
6770
<add name="connectionStrings:jsonConnectionString1" connectionString="Will only be replaced by 'Json' in 'Flat' jsonMode." />
6871
<add name="jsonConnectionString2" connectionString="Will only be replaced by 'Json' in 'Sectional' jsonMode." />

src/Base/KeyValueConfigBuilder.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Collections.Specialized;
88
using System.Text.RegularExpressions;
99
using System.Xml;
10+
using System.Security;
1011

1112
namespace Microsoft.Configuration.ConfigurationBuilders
1213
{
@@ -23,7 +24,8 @@ public abstract class KeyValueConfigBuilder : ConfigurationBuilder
2324
public const string stripPrefixTag = "stripPrefix";
2425
public const string tokenPatternTag = "tokenPattern";
2526
public const string optionalTag = "optional";
26-
#pragma warning restore CS1591 // No xml comments for tag literals.
27+
public const string escapeTag = "escapeExpandedValues";
28+
#pragma warning restore CS1591 // No xml comments for tag literals.
2729

2830
private NameValueCollection _config = null;
2931
private IDictionary<string, string> _cachedValues;
@@ -50,6 +52,11 @@ public abstract class KeyValueConfigBuilder : ConfigurationBuilder
5052
public bool Optional { get { EnsureInitialized(); return _optional; } protected set { _optional = value; } }
5153
private bool _optional = true;
5254
/// <summary>
55+
/// Specifies whether the config builder should cause errors if the backing source cannot be found.
56+
/// </summary>
57+
public bool EscapeValues { get { EnsureInitialized(); return _escapeValues; } protected set { _escapeValues = value; } }
58+
private bool _escapeValues = false;
59+
/// <summary>
5360
/// Gets or sets a regular expression used for matching tokens in raw xml during Greedy substitution.
5461
/// </summary>
5562
public string TokenPattern { get { EnsureInitialized(); return _tokenPattern; } protected set { _tokenPattern = value; } }
@@ -113,6 +120,7 @@ protected virtual void LazyInitialize(string name, NameValueCollection config)
113120
_keyPrefix = UpdateConfigSettingWithAppSettings(prefixTag) ?? _keyPrefix;
114121
_stripPrefix = (UpdateConfigSettingWithAppSettings(stripPrefixTag) != null) ? Boolean.Parse(config[stripPrefixTag]) : _stripPrefix;
115122
_optional = (UpdateConfigSettingWithAppSettings(optionalTag) != null) ? Boolean.Parse(config[optionalTag]) : _optional;
123+
_escapeValues = (UpdateConfigSettingWithAppSettings(escapeTag) != null) ? Boolean.Parse(config[escapeTag]) : _escapeValues;
116124

117125
_cachedValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
118126
_lazyInitialized = true;
@@ -278,7 +286,7 @@ private XmlNode ExpandTokens(XmlNode rawXml)
278286

279287
// Same prefix-handling rules apply in expand mode as in strict mode.
280288
// Since the key is being completely replaced by the value, we don't need to call UpdateKey().
281-
return GetValueInternal(key) ?? m.Groups[0].Value;
289+
return EscapeValue(GetValueInternal(key)) ?? m.Groups[0].Value;
282290
});
283291

284292
XmlDocument doc = new XmlDocument();
@@ -321,6 +329,12 @@ private string TrimPrefix(string fullString)
321329
return fullString.Substring(KeyPrefix.Length);
322330
}
323331

332+
// Maybe this could be virtual? Simple xml escaping should be enough for most folks.
333+
private string EscapeValue(string original)
334+
{
335+
return (_escapeValues && original != null) ? SecurityElement.Escape(original) : original;
336+
}
337+
324338
#endregion
325339
//=========================================================================================================================
326340
}

0 commit comments

Comments
 (0)