// Copyright (c) 2019, David Kitchen // // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, this // list of conditions and the following disclaimer. // // * Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // // * Neither the name of the organisation (Microcosm) nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. package css import ( "strings" "testing" "time" ) // TestIssue225 is a regression test for // https://github.com/microcosm-cc/bluemonday/issues/225. // // Shorthand property handlers such as FontHandler tokenize their value // and feed it to recursiveCheck, which searches for a way to partition // the tokens so that every group is accepted by one of a handful of // functions. A style value whose tokenization produces several // whitespace-only entries (as happens when a declaration is indented // with newlines and runs of spaces, like the payload in issue #225) // gives that search many equally valid split points, since a // whitespace-only entry is accepted as an "empty" font-family by // FontFamilyHandler. Before this fix, recursiveCheck re-explored the // same remaining tokens once per partition path that reached them, // making it exponential in the number of tokens: on the vulnerable // revision, 20 blank tokens took upwards of 15 seconds to evaluate // (measured via the full bluemonday.Policy.Sanitize pipeline). // // This test guards against that regressing: it must complete well // within the deadline below on a fixed/patched recursiveCheck, and it // times out (rather than hanging for the several seconds the // unmemoized search would take) if the exponential behaviour returns. // // Note that the pathological value below is ultimately rejected // (FontHandler returns false for it, confirmed against both the fixed // and the pre-fix code): the vulnerability is not about a value being // wrongly accepted, it is that recursiveCheck could take exponential // time to reach *either* verdict. Measured directly against FontHandler // on the vulnerable revision, this exact value took ~4s; the equivalent // value with 20 spaces of indentation took ~15s end-to-end through // bluemonday.Policy.Sanitize. func TestIssue225(t *testing.T) { // 20 blank tokens, produced the same way the issue #225 payload // produced them: a run of spaces immediately after a newline. pathological := "normal\n" + strings.Repeat(" ", 20) + "16px\nroboto" const deadline = 3 * time.Second const want = false result := make(chan bool, 1) go func() { result <- FontHandler(pathological) }() select { case got := <-result: if got != want { t.Errorf("FontHandler(%q) = %v, want %v", pathological, got, want) } case <-time.After(deadline): t.Fatalf("FontHandler did not return within %v for a 20-blank-token "+ "value; recursiveCheck may have regressed to exponential-time "+ "behaviour (see issue #225)", deadline) } } // TestIssue225AcceptanceUnchanged asserts that the recursiveCheck // memoization added to fix issue #225 does not change which values are // accepted or rejected by a shorthand handler; it only changes how fast // that determination is made. Expected results were captured from // FontHandler's existing (pre-fix) behaviour. func TestIssue225AcceptanceUnchanged(t *testing.T) { tests := []struct { value string want bool }{ // Representative valid "font" shorthand values (the first two // mirror cases already exercised end-to-end in // TestStyling in sanitize_test.go). {"italic bold 12px/30px Georgia, serif", true}, {"icon", true}, {"caption", true}, {"normal 16px Roboto", true}, {"bold 16px/1.5 Arial, sans-serif", true}, // Representative invalid/unsafe values that must stay rejected. {"expression(alert(1))", false}, {"javascript:alert(1)", false}, {"url(evil.com)", false}, } for _, tt := range tests { if got := FontHandler(tt.value); got != tt.want { t.Errorf("FontHandler(%q) = %v, want %v", tt.value, got, tt.want) } } }