From a58b4867b51a03842022495609c1e7d16f46ff0f Mon Sep 17 00:00:00 2001 From: Ryan Polley Date: Wed, 13 Jan 2021 11:43:43 -0600 Subject: [PATCH] Sanitize XML token replacements in templates --- .../services/TemplateServiceSpecs.cs | 56 +++++++++++++++++++ .../services/TemplateService.cs | 37 ++++++++---- .../ChocolateyBeforeModifyTemplate.cs | 1 + .../templates/ChocolateyInstallTemplate.cs | 1 + .../ChocolateyLicenseFileTemplate.cs | 1 + .../templates/ChocolateyReadMeTemplate.cs | 1 + .../templates/ChocolateyTodoTemplate.cs | 1 + .../templates/ChocolateyUninstallTemplate.cs | 1 + .../ChocolateyVerificationFileTemplate.cs | 1 + .../templates/ITokenEscaper.cs | 12 ++++ .../templates/NuspecTemplate.cs | 1 + .../templates/PlaintextTokenEscaper.cs | 16 ++++++ .../templates/TemplateLanguage.cs | 41 ++++++++++++++ .../templates/XMLTokenEscaper.cs | 17 ++++++ .../infrastructure/tokens/TokenReplacer.cs | 7 ++- 15 files changed, 180 insertions(+), 14 deletions(-) create mode 100644 src/chocolatey/infrastructure.app/templates/ITokenEscaper.cs create mode 100644 src/chocolatey/infrastructure.app/templates/PlaintextTokenEscaper.cs create mode 100644 src/chocolatey/infrastructure.app/templates/TemplateLanguage.cs create mode 100644 src/chocolatey/infrastructure.app/templates/XMLTokenEscaper.cs diff --git a/src/chocolatey.tests/infrastructure.app/services/TemplateServiceSpecs.cs b/src/chocolatey.tests/infrastructure.app/services/TemplateServiceSpecs.cs index fb9f9f9659..f0cb29e87f 100644 --- a/src/chocolatey.tests/infrastructure.app/services/TemplateServiceSpecs.cs +++ b/src/chocolatey.tests/infrastructure.app/services/TemplateServiceSpecs.cs @@ -147,6 +147,62 @@ public void should_log_info_if_regular_output() } } + public class when_generate_file_from_template_is_called_on_xml_with_special_characters : TemplateServiceSpecsBase + { + private Action because; + private readonly ChocolateyConfiguration config = new ChocolateyConfiguration(); + private readonly TemplateValues templateValues = new TemplateValues(); + private readonly string template = "[[PackageName]]"; + private const string fileLocation = "c:\\packages\\bob<>&'\".nuspec"; + private string fileContent; + + public override void Context() + { + base.Context(); + + fileSystem.Setup(x => x.write_file(It.Is((string fl) => fl == fileLocation), It.IsAny(), Encoding.UTF8)) + .Callback((string filePath, string fileContent, Encoding encoding) => this.fileContent = fileContent); + + templateValues.PackageName = "Bob<>&'\""; + } + + public override void Because() + { + because = () => service.generate_file_from_template(config, templateValues, template, fileLocation, Encoding.UTF8, TemplateLanguage.XML); + } + + public override void BeforeEachSpec() + { + MockLogger.reset(); + } + + [Fact] + public void should_write_file_with_escaped_and_replaced_tokens() + { + because(); + + var debugs = MockLogger.MessagesFor(LogLevel.Debug); + debugs.Count.ShouldEqual(1); + debugs[0].ShouldEqual("Bob<>&'""); + } + + [Fact] + public void should_log_info_if_regular_output() + { + config.RegularOutput = true; + + because(); + + var debugs = MockLogger.MessagesFor(LogLevel.Debug); + debugs.Count.ShouldEqual(1); + debugs[0].ShouldEqual("Bob<>&'""); + + var infos = MockLogger.MessagesFor(LogLevel.Info); + infos.Count.ShouldEqual(1); + infos[0].ShouldEqual(string.Format(@"Generating template to a file{0} at 'c:\packages\bob<>&'"".nuspec'", Environment.NewLine)); + } + } + public class when_generate_is_called_with_existing_directory : TemplateServiceSpecsBase { private Action because; diff --git a/src/chocolatey/infrastructure.app/services/TemplateService.cs b/src/chocolatey/infrastructure.app/services/TemplateService.cs index 3c5095e267..66ffeef6fc 100644 --- a/src/chocolatey/infrastructure.app/services/TemplateService.cs +++ b/src/chocolatey/infrastructure.app/services/TemplateService.cs @@ -104,14 +104,14 @@ public void generate(ChocolateyConfiguration configuration) var defaultTemplateOverride = _fileSystem.combine_paths(ApplicationParameters.TemplatesLocation, "default"); if (string.IsNullOrWhiteSpace(configuration.NewCommand.TemplateName) && (!_fileSystem.directory_exists(defaultTemplateOverride) || configuration.NewCommand.UseOriginalTemplate)) { - generate_file_from_template(configuration, tokens, NuspecTemplate.Template, _fileSystem.combine_paths(packageLocation, "{0}.nuspec".format_with(tokens.PackageNameLower)), Encoding.UTF8); - generate_file_from_template(configuration, tokens, ChocolateyInstallTemplate.Template, _fileSystem.combine_paths(packageToolsLocation, "chocolateyinstall.ps1"), Encoding.UTF8); - generate_file_from_template(configuration, tokens, ChocolateyBeforeModifyTemplate.Template, _fileSystem.combine_paths(packageToolsLocation, "chocolateybeforemodify.ps1"), Encoding.UTF8); - generate_file_from_template(configuration, tokens, ChocolateyUninstallTemplate.Template, _fileSystem.combine_paths(packageToolsLocation, "chocolateyuninstall.ps1"), Encoding.UTF8); - generate_file_from_template(configuration, tokens, ChocolateyLicenseFileTemplate.Template, _fileSystem.combine_paths(packageToolsLocation, "LICENSE.txt"), Encoding.UTF8); - generate_file_from_template(configuration, tokens, ChocolateyVerificationFileTemplate.Template, _fileSystem.combine_paths(packageToolsLocation, "VERIFICATION.txt"), Encoding.UTF8); - generate_file_from_template(configuration, tokens, ChocolateyReadMeTemplate.Template, _fileSystem.combine_paths(packageLocation, "ReadMe.md"), Encoding.UTF8); - generate_file_from_template(configuration, tokens, ChocolateyTodoTemplate.Template, _fileSystem.combine_paths(packageLocation, "_TODO.txt"), Encoding.UTF8); + generate_file_from_template(configuration, tokens, NuspecTemplate.Template, _fileSystem.combine_paths(packageLocation, "{0}.nuspec".format_with(tokens.PackageNameLower)), Encoding.UTF8, NuspecTemplate.Language); + generate_file_from_template(configuration, tokens, ChocolateyInstallTemplate.Template, _fileSystem.combine_paths(packageToolsLocation, "chocolateyinstall.ps1"), Encoding.UTF8, ChocolateyInstallTemplate.Language); + generate_file_from_template(configuration, tokens, ChocolateyBeforeModifyTemplate.Template, _fileSystem.combine_paths(packageToolsLocation, "chocolateybeforemodify.ps1"), Encoding.UTF8, ChocolateyBeforeModifyTemplate.Language); + generate_file_from_template(configuration, tokens, ChocolateyUninstallTemplate.Template, _fileSystem.combine_paths(packageToolsLocation, "chocolateyuninstall.ps1"), Encoding.UTF8, ChocolateyUninstallTemplate.Language); + generate_file_from_template(configuration, tokens, ChocolateyLicenseFileTemplate.Template, _fileSystem.combine_paths(packageToolsLocation, "LICENSE.txt"), Encoding.UTF8, ChocolateyLicenseFileTemplate.Language); + generate_file_from_template(configuration, tokens, ChocolateyVerificationFileTemplate.Template, _fileSystem.combine_paths(packageToolsLocation, "VERIFICATION.txt"), Encoding.UTF8, ChocolateyVerificationFileTemplate.Language); + generate_file_from_template(configuration, tokens, ChocolateyReadMeTemplate.Template, _fileSystem.combine_paths(packageLocation, "ReadMe.md"), Encoding.UTF8, ChocolateyReadMeTemplate.Language); + generate_file_from_template(configuration, tokens, ChocolateyTodoTemplate.Template, _fileSystem.combine_paths(packageLocation, "_TODO.txt"), Encoding.UTF8, ChocolateyTodoTemplate.Language); } else { @@ -134,7 +134,7 @@ public void generate(ChocolateyConfiguration configuration) } else { - generate_file_from_template(configuration, tokens, _fileSystem.read_file(file), packageFileLocation, Encoding.UTF8); + generate_file_from_template(configuration, tokens, _fileSystem.read_file(file), packageFileLocation, Encoding.UTF8, guessLanguageFromExtension(fileExtension)); } } } @@ -144,10 +144,23 @@ public void generate(ChocolateyConfiguration configuration) configuration.NewCommand.Name, configuration.NewCommand.AutomaticPackage ? " (automatic)" : string.Empty, Environment.NewLine, packageLocation)); } - public void generate_file_from_template(ChocolateyConfiguration configuration, TemplateValues tokens, string template, string fileLocation, Encoding encoding) + TemplateLanguage guessLanguageFromExtension(string extension) { - template = TokenReplacer.replace_tokens(tokens, template); - template = TokenReplacer.replace_tokens(tokens.AdditionalProperties, template); + switch (extension) + { + case "xml": + case "nuspec": + return TemplateLanguage.XML; + default: + return TemplateLanguage.PlainText; + } + } + + public void generate_file_from_template(ChocolateyConfiguration configuration, TemplateValues tokens, string template, string fileLocation, Encoding encoding, TemplateLanguage language = TemplateLanguage.PlainText) + { + + template = TokenReplacer.replace_tokens(tokens, template, targetLanguage: language); + template = TokenReplacer.replace_tokens(tokens.AdditionalProperties, template, targetLanguage: language); if (configuration.RegularOutput) this.Log().Info(() => "Generating template to a file{0} at '{1}'".format_with(Environment.NewLine, fileLocation)); this.Log().Debug(() => "{0}".format_with(template)); diff --git a/src/chocolatey/infrastructure.app/templates/ChocolateyBeforeModifyTemplate.cs b/src/chocolatey/infrastructure.app/templates/ChocolateyBeforeModifyTemplate.cs index 64025426c1..084d1e15fb 100644 --- a/src/chocolatey/infrastructure.app/templates/ChocolateyBeforeModifyTemplate.cs +++ b/src/chocolatey/infrastructure.app/templates/ChocolateyBeforeModifyTemplate.cs @@ -18,6 +18,7 @@ namespace chocolatey.infrastructure.app.templates { public class ChocolateyBeforeModifyTemplate { + public static TemplateLanguage Language = TemplateLanguage.PlainText; public static string Template = @"# This runs in 0.9.10+ before upgrade and uninstall. # Use this file to do things like stop services prior to upgrade or uninstall. diff --git a/src/chocolatey/infrastructure.app/templates/ChocolateyInstallTemplate.cs b/src/chocolatey/infrastructure.app/templates/ChocolateyInstallTemplate.cs index 7dbcdc2c95..a62159ee04 100644 --- a/src/chocolatey/infrastructure.app/templates/ChocolateyInstallTemplate.cs +++ b/src/chocolatey/infrastructure.app/templates/ChocolateyInstallTemplate.cs @@ -18,6 +18,7 @@ namespace chocolatey.infrastructure.app.templates { public class ChocolateyInstallTemplate { + public static TemplateLanguage Language = TemplateLanguage.PlainText; public static string Template = @"# IMPORTANT: Before releasing this package, copy/paste the next 2 lines into PowerShell to remove all comments from this file: # $f='c:\path\to\thisFile.ps1' diff --git a/src/chocolatey/infrastructure.app/templates/ChocolateyLicenseFileTemplate.cs b/src/chocolatey/infrastructure.app/templates/ChocolateyLicenseFileTemplate.cs index 401cf48a2e..4b47c37c18 100644 --- a/src/chocolatey/infrastructure.app/templates/ChocolateyLicenseFileTemplate.cs +++ b/src/chocolatey/infrastructure.app/templates/ChocolateyLicenseFileTemplate.cs @@ -18,6 +18,7 @@ namespace chocolatey.infrastructure.app.templates { public class ChocolateyLicenseFileTemplate { + public static TemplateLanguage Language = TemplateLanguage.PlainText; public static string Template = @" Note: Include this file if including binaries you have the right to distribute. diff --git a/src/chocolatey/infrastructure.app/templates/ChocolateyReadMeTemplate.cs b/src/chocolatey/infrastructure.app/templates/ChocolateyReadMeTemplate.cs index fd3e36fffd..632decbc7b 100644 --- a/src/chocolatey/infrastructure.app/templates/ChocolateyReadMeTemplate.cs +++ b/src/chocolatey/infrastructure.app/templates/ChocolateyReadMeTemplate.cs @@ -18,6 +18,7 @@ namespace chocolatey.infrastructure.app.templates { public class ChocolateyReadMeTemplate { + public static TemplateLanguage Language = TemplateLanguage.PlainText; public static string Template = @"## Summary How do I create packages? See https://chocolatey.org/docs/create-packages diff --git a/src/chocolatey/infrastructure.app/templates/ChocolateyTodoTemplate.cs b/src/chocolatey/infrastructure.app/templates/ChocolateyTodoTemplate.cs index d03698a29d..7138065aee 100644 --- a/src/chocolatey/infrastructure.app/templates/ChocolateyTodoTemplate.cs +++ b/src/chocolatey/infrastructure.app/templates/ChocolateyTodoTemplate.cs @@ -18,6 +18,7 @@ namespace chocolatey.infrastructure.app.templates { public class ChocolateyTodoTemplate { + public static TemplateLanguage Language = TemplateLanguage.PlainText; public static string Template = @"TODO diff --git a/src/chocolatey/infrastructure.app/templates/ChocolateyUninstallTemplate.cs b/src/chocolatey/infrastructure.app/templates/ChocolateyUninstallTemplate.cs index 715212260a..ac69ed0892 100644 --- a/src/chocolatey/infrastructure.app/templates/ChocolateyUninstallTemplate.cs +++ b/src/chocolatey/infrastructure.app/templates/ChocolateyUninstallTemplate.cs @@ -18,6 +18,7 @@ namespace chocolatey.infrastructure.app.templates { public class ChocolateyUninstallTemplate { + public static TemplateLanguage Language = TemplateLanguage.PlainText; public static string Template = @"# IMPORTANT: Before releasing this package, copy/paste the next 2 lines into PowerShell to remove all comments from this file: # $f='c:\path\to\thisFile.ps1' diff --git a/src/chocolatey/infrastructure.app/templates/ChocolateyVerificationFileTemplate.cs b/src/chocolatey/infrastructure.app/templates/ChocolateyVerificationFileTemplate.cs index 4c82170c80..901edaac2a 100644 --- a/src/chocolatey/infrastructure.app/templates/ChocolateyVerificationFileTemplate.cs +++ b/src/chocolatey/infrastructure.app/templates/ChocolateyVerificationFileTemplate.cs @@ -18,6 +18,7 @@ namespace chocolatey.infrastructure.app.templates { public class ChocolateyVerificationFileTemplate { + public static TemplateLanguage Language = TemplateLanguage.PlainText; public static string Template = @" Note: Include this file if including binaries you have the right to distribute. Otherwise delete. this file. If you are the software author, you can change this diff --git a/src/chocolatey/infrastructure.app/templates/ITokenEscaper.cs b/src/chocolatey/infrastructure.app/templates/ITokenEscaper.cs new file mode 100644 index 0000000000..5beef6b403 --- /dev/null +++ b/src/chocolatey/infrastructure.app/templates/ITokenEscaper.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace chocolatey.infrastructure.app.templates +{ + public interface ITokenEscaper + { + string escape(string toEscape); + } +} diff --git a/src/chocolatey/infrastructure.app/templates/NuspecTemplate.cs b/src/chocolatey/infrastructure.app/templates/NuspecTemplate.cs index 4ab6dc8a94..a25eea2121 100644 --- a/src/chocolatey/infrastructure.app/templates/NuspecTemplate.cs +++ b/src/chocolatey/infrastructure.app/templates/NuspecTemplate.cs @@ -18,6 +18,7 @@ namespace chocolatey.infrastructure.app.templates { public class NuspecTemplate { + public static TemplateLanguage Language = TemplateLanguage.XML; public static string Template = @" diff --git a/src/chocolatey/infrastructure.app/templates/PlaintextTokenEscaper.cs b/src/chocolatey/infrastructure.app/templates/PlaintextTokenEscaper.cs new file mode 100644 index 0000000000..c8bb7ae494 --- /dev/null +++ b/src/chocolatey/infrastructure.app/templates/PlaintextTokenEscaper.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace chocolatey.infrastructure.app.templates +{ + // Represents the escaping strategy of not escaping anything + class PlaintextTokenEscaper : ITokenEscaper + { + public string escape(string toEscape) + { + return toEscape; + } + } +} diff --git a/src/chocolatey/infrastructure.app/templates/TemplateLanguage.cs b/src/chocolatey/infrastructure.app/templates/TemplateLanguage.cs new file mode 100644 index 0000000000..ed2ba03e9e --- /dev/null +++ b/src/chocolatey/infrastructure.app/templates/TemplateLanguage.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +// Copyright © 2020 Chocolatey Software, Inc +// Copyright © 2020 RealDimensions Software, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +namespace chocolatey.infrastructure.app.templates +{ + public static class Extensions + { + // get the escaper used for a given language + public static ITokenEscaper GetTokenEscaper(this TemplateLanguage language) + { + switch (language) { + case TemplateLanguage.XML: + return new XMLTokenEscaper(); + default: + return new PlaintextTokenEscaper(); + } + } + } + public enum TemplateLanguage + { + XML, + PlainText + } +} diff --git a/src/chocolatey/infrastructure.app/templates/XMLTokenEscaper.cs b/src/chocolatey/infrastructure.app/templates/XMLTokenEscaper.cs new file mode 100644 index 0000000000..0034a73730 --- /dev/null +++ b/src/chocolatey/infrastructure.app/templates/XMLTokenEscaper.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Security; + +namespace chocolatey.infrastructure.app.templates +{ + //escape strings so that + class XMLTokenEscaper : ITokenEscaper + { + public string escape(string toEscape) + { + return SecurityElement.Escape(toEscape); + } + } +} diff --git a/src/chocolatey/infrastructure/tokens/TokenReplacer.cs b/src/chocolatey/infrastructure/tokens/TokenReplacer.cs index e4956eed96..1eba069635 100644 --- a/src/chocolatey/infrastructure/tokens/TokenReplacer.cs +++ b/src/chocolatey/infrastructure/tokens/TokenReplacer.cs @@ -16,19 +16,22 @@ namespace chocolatey.infrastructure.tokens { + using chocolatey.infrastructure.app.templates; using System.Collections.Generic; using System.Reflection; using System.Text.RegularExpressions; public sealed class TokenReplacer { - public static string replace_tokens(TConfig configuration, string textToReplace, string tokenPrefix = "[[", string tokenSuffix = "]]") + public static string replace_tokens(TConfig configuration, string textToReplace, string tokenPrefix = "[[", string tokenSuffix = "]]", TemplateLanguage targetLanguage = TemplateLanguage.PlainText) { if (string.IsNullOrEmpty(textToReplace)) return string.Empty; IDictionary dictionary = create_dictionary_from_configuration(configuration); if (dictionary.Count == 0) return textToReplace; + ITokenEscaper tokenEscaper = targetLanguage.GetTokenEscaper(); + var regex = new Regex("{0}(?\\w+){1}".format_with(Regex.Escape(tokenPrefix), Regex.Escape(tokenSuffix))); string output = regex.Replace(textToReplace, m => @@ -43,7 +46,7 @@ public static string replace_tokens(TConfig configuration, string textT } string value = dictionary[key]; - return value; + return tokenEscaper.escape(value); }); return output;