From f6adbf6cfa6f5870283fa37e8504f0cc21b6f9ad Mon Sep 17 00:00:00 2001 From: FernandoSendMail Date: Sun, 29 Mar 2015 02:08:36 -0700 Subject: [PATCH 01/14] fix(typeahead): Typeahead uses ngSanitize when present --- karma.conf.js | 1 + misc/demo/index.html | 1 + misc/test-lib/angular-sanitize.js | 681 ++++++++++++++++++ .../typeahead-highlight-ngsanitize.spec.js | 19 + .../test/typeahead-highlight.spec.js | 31 +- src/typeahead/typeahead.js | 25 +- template/typeahead/typeahead-match.html | 2 +- 7 files changed, 745 insertions(+), 15 deletions(-) create mode 100644 misc/test-lib/angular-sanitize.js create mode 100644 src/typeahead/test/typeahead-highlight-ngsanitize.spec.js diff --git a/karma.conf.js b/karma.conf.js index ed3d462618..7f6b265f8e 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -9,6 +9,7 @@ files = [ 'misc/test-lib/jquery-1.8.2.min.js', 'misc/test-lib/angular.js', 'misc/test-lib/angular-mocks.js', + 'misc/test-lib/angular-sanitize.js', 'misc/test-lib/helpers.js', 'src/**/*.js', 'template/**/*.js' diff --git a/misc/demo/index.html b/misc/demo/index.html index 5d2cc0ed22..a8bd55fc20 100644 --- a/misc/demo/index.html +++ b/misc/demo/index.html @@ -14,6 +14,7 @@ + diff --git a/misc/test-lib/angular-sanitize.js b/misc/test-lib/angular-sanitize.js new file mode 100644 index 0000000000..b629a52433 --- /dev/null +++ b/misc/test-lib/angular-sanitize.js @@ -0,0 +1,681 @@ +/** + * @license AngularJS v1.3.13 + * (c) 2010-2014 Google, Inc. http://angularjs.org + * License: MIT + */ +(function(window, angular, undefined) {'use strict'; + +var $sanitizeMinErr = angular.$$minErr('$sanitize'); + +/** + * @ngdoc module + * @name ngSanitize + * @description + * + * # ngSanitize + * + * The `ngSanitize` module provides functionality to sanitize HTML. + * + * + *
+ * + * See {@link ngSanitize.$sanitize `$sanitize`} for usage. + */ + +/* + * HTML Parser By Misko Hevery (misko@hevery.com) + * based on: HTML Parser By John Resig (ejohn.org) + * Original code by Erik Arvidsson, Mozilla Public License + * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js + * + * // Use like so: + * htmlParser(htmlString, { + * start: function(tag, attrs, unary) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * }); + * + */ + + +/** + * @ngdoc service + * @name $sanitize + * @kind function + * + * @description + * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are + * then serialized back to properly escaped html string. This means that no unsafe input can make + * it into the returned string, however, since our parser is more strict than a typical browser + * parser, it's possible that some obscure input, which would be recognized as valid HTML by a + * browser, won't make it through the sanitizer. The input may also contain SVG markup. + * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and + * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}. + * + * @param {string} html HTML input. + * @returns {string} Sanitized HTML. + * + * @example + + + +
+ Snippet: + + + + + + + + + + + + + + + + + + + + + + + + + +
DirectiveHowSourceRendered
ng-bind-htmlAutomatically uses $sanitize
<div ng-bind-html="snippet">
</div>
ng-bind-htmlBypass $sanitize by explicitly trusting the dangerous value +
<div ng-bind-html="deliberatelyTrustDangerousSnippet()">
+</div>
+
ng-bindAutomatically escapes
<div ng-bind="snippet">
</div>
+
+
+ + it('should sanitize the html snippet by default', function() { + expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). + toBe('

an html\nclick here\nsnippet

'); + }); + + it('should inline raw snippet if bound to a trusted value', function() { + expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()). + toBe("

an html\n" + + "click here\n" + + "snippet

"); + }); + + it('should escape snippet without any filter', function() { + expect(element(by.css('#bind-default div')).getInnerHtml()). + toBe("<p style=\"color:blue\">an html\n" + + "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + + "snippet</p>"); + }); + + it('should update', function() { + element(by.model('snippet')).clear(); + element(by.model('snippet')).sendKeys('new text'); + expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). + toBe('new text'); + expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe( + 'new text'); + expect(element(by.css('#bind-default div')).getInnerHtml()).toBe( + "new <b onclick=\"alert(1)\">text</b>"); + }); +
+
+ */ +function $SanitizeProvider() { + this.$get = ['$$sanitizeUri', function($$sanitizeUri) { + return function(html) { + var buf = []; + htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) { + return !/^unsafe/.test($$sanitizeUri(uri, isImage)); + })); + return buf.join(''); + }; + }]; +} + +function sanitizeText(chars) { + var buf = []; + var writer = htmlSanitizeWriter(buf, angular.noop); + writer.chars(chars); + return buf.join(''); +} + + +// Regular Expressions for parsing tags and attributes +var START_TAG_REGEXP = + /^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/, + END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/, + ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, + BEGIN_TAG_REGEXP = /^/g, + DOCTYPE_REGEXP = /]*?)>/i, + CDATA_REGEXP = //g, + SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, + // Match everything outside of normal chars and " (quote character) + NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; + + +// Good source of info about elements and attributes +// http://dev.w3.org/html5/spec/Overview.html#semantics +// http://simon.html5.org/html-elements + +// Safe Void Elements - HTML5 +// http://dev.w3.org/html5/spec/Overview.html#void-elements +var voidElements = makeMap("area,br,col,hr,img,wbr"); + +// Elements that you can, intentionally, leave open (and which close themselves) +// http://dev.w3.org/html5/spec/Overview.html#optional-tags +var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), + optionalEndTagInlineElements = makeMap("rp,rt"), + optionalEndTagElements = angular.extend({}, + optionalEndTagInlineElements, + optionalEndTagBlockElements); + +// Safe Block Elements - HTML5 +var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," + + "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," + + "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); + +// Inline Elements - HTML5 +var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," + + "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," + + "samp,small,span,strike,strong,sub,sup,time,tt,u,var")); + +// SVG Elements +// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements +var svgElements = makeMap("animate,animateColor,animateMotion,animateTransform,circle,defs," + + "desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,hkern,image,linearGradient," + + "line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,radialGradient,rect,set," + + "stop,svg,switch,text,title,tspan,use"); + +// Special Elements (can contain anything) +var specialElements = makeMap("script,style"); + +var validElements = angular.extend({}, + voidElements, + blockElements, + inlineElements, + optionalEndTagElements, + svgElements); + +//Attributes that have href and hence need to be sanitized +var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap,xlink:href"); + +var htmlAttrs = makeMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' + + 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' + + 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' + + 'scope,scrolling,shape,size,span,start,summary,target,title,type,' + + 'valign,value,vspace,width'); + +// SVG attributes (without "id" and "name" attributes) +// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes +var svgAttrs = makeMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' + + 'attributeName,attributeType,baseProfile,bbox,begin,by,calcMode,cap-height,class,color,' + + 'color-rendering,content,cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,' + + 'font-size,font-stretch,font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,' + + 'gradientUnits,hanging,height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,' + + 'keySplines,keyTimes,lang,marker-end,marker-mid,marker-start,markerHeight,markerUnits,' + + 'markerWidth,mathematical,max,min,offset,opacity,orient,origin,overline-position,' + + 'overline-thickness,panose-1,path,pathLength,points,preserveAspectRatio,r,refX,refY,' + + 'repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,' + + 'stemv,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke,' + + 'stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,' + + 'stroke-opacity,stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,' + + 'underline-position,underline-thickness,unicode,unicode-range,units-per-em,values,version,' + + 'viewBox,visibility,width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,' + + 'xlink:show,xlink:title,xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,' + + 'zoomAndPan'); + +var validAttrs = angular.extend({}, + uriAttrs, + svgAttrs, + htmlAttrs); + +function makeMap(str) { + var obj = {}, items = str.split(','), i; + for (i = 0; i < items.length; i++) obj[items[i]] = true; + return obj; +} + + +/** + * @example + * htmlParser(htmlString, { + * start: function(tag, attrs, unary) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * }); + * + * @param {string} html string + * @param {object} handler + */ +function htmlParser(html, handler) { + if (typeof html !== 'string') { + if (html === null || typeof html === 'undefined') { + html = ''; + } else { + html = '' + html; + } + } + var index, chars, match, stack = [], last = html, text; + stack.last = function() { return stack[stack.length - 1]; }; + + while (html) { + text = ''; + chars = true; + + // Make sure we're not in a script or style element + if (!stack.last() || !specialElements[stack.last()]) { + + // Comment + if (html.indexOf("", index) === index) { + if (handler.comment) handler.comment(html.substring(4, index)); + html = html.substring(index + 3); + chars = false; + } + // DOCTYPE + } else if (DOCTYPE_REGEXP.test(html)) { + match = html.match(DOCTYPE_REGEXP); + + if (match) { + html = html.replace(match[0], ''); + chars = false; + } + // end tag + } else if (BEGING_END_TAGE_REGEXP.test(html)) { + match = html.match(END_TAG_REGEXP); + + if (match) { + html = html.substring(match[0].length); + match[0].replace(END_TAG_REGEXP, parseEndTag); + chars = false; + } + + // start tag + } else if (BEGIN_TAG_REGEXP.test(html)) { + match = html.match(START_TAG_REGEXP); + + if (match) { + // We only have a valid start-tag if there is a '>'. + if (match[4]) { + html = html.substring(match[0].length); + match[0].replace(START_TAG_REGEXP, parseStartTag); + } + chars = false; + } else { + // no ending tag found --- this piece should be encoded as an entity. + text += '<'; + html = html.substring(1); + } + } + + if (chars) { + index = html.indexOf("<"); + + text += index < 0 ? html : html.substring(0, index); + html = index < 0 ? "" : html.substring(index); + + if (handler.chars) handler.chars(decodeEntities(text)); + } + + } else { + // IE versions 9 and 10 do not understand the regex '[^]', so using a workaround with [\W\w]. + html = html.replace(new RegExp("([\\W\\w]*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), + function(all, text) { + text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1"); + + if (handler.chars) handler.chars(decodeEntities(text)); + + return ""; + }); + + parseEndTag("", stack.last()); + } + + if (html == last) { + throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " + + "of html: {0}", html); + } + last = html; + } + + // Clean up any remaining tags + parseEndTag(); + + function parseStartTag(tag, tagName, rest, unary) { + tagName = angular.lowercase(tagName); + if (blockElements[tagName]) { + while (stack.last() && inlineElements[stack.last()]) { + parseEndTag("", stack.last()); + } + } + + if (optionalEndTagElements[tagName] && stack.last() == tagName) { + parseEndTag("", tagName); + } + + unary = voidElements[tagName] || !!unary; + + if (!unary) + stack.push(tagName); + + var attrs = {}; + + rest.replace(ATTR_REGEXP, + function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) { + var value = doubleQuotedValue + || singleQuotedValue + || unquotedValue + || ''; + + attrs[name] = decodeEntities(value); + }); + if (handler.start) handler.start(tagName, attrs, unary); + } + + function parseEndTag(tag, tagName) { + var pos = 0, i; + tagName = angular.lowercase(tagName); + if (tagName) + // Find the closest opened tag of the same type + for (pos = stack.length - 1; pos >= 0; pos--) + if (stack[pos] == tagName) + break; + + if (pos >= 0) { + // Close all the open elements, up the stack + for (i = stack.length - 1; i >= pos; i--) + if (handler.end) handler.end(stack[i]); + + // Remove the open elements from the stack + stack.length = pos; + } + } +} + +var hiddenPre=document.createElement("pre"); +var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/; +/** + * decodes all entities into regular string + * @param value + * @returns {string} A string with decoded entities. + */ +function decodeEntities(value) { + if (!value) { return ''; } + + // Note: IE8 does not preserve spaces at the start/end of innerHTML + // so we must capture them and reattach them afterward + var parts = spaceRe.exec(value); + var spaceBefore = parts[1]; + var spaceAfter = parts[3]; + var content = parts[2]; + if (content) { + hiddenPre.innerHTML=content.replace(//g, '>'); +} + +/** + * create an HTML/XML writer which writes to buffer + * @param {Array} buf use buf.jain('') to get out sanitized html string + * @returns {object} in the form of { + * start: function(tag, attrs, unary) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * } + */ +function htmlSanitizeWriter(buf, uriValidator) { + var ignore = false; + var out = angular.bind(buf, buf.push); + return { + start: function(tag, attrs, unary) { + tag = angular.lowercase(tag); + if (!ignore && specialElements[tag]) { + ignore = tag; + } + if (!ignore && validElements[tag] === true) { + out('<'); + out(tag); + angular.forEach(attrs, function(value, key) { + var lkey=angular.lowercase(key); + var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background'); + if (validAttrs[lkey] === true && + (uriAttrs[lkey] !== true || uriValidator(value, isImage))) { + out(' '); + out(key); + out('="'); + out(encodeEntities(value)); + out('"'); + } + }); + out(unary ? '/>' : '>'); + } + }, + end: function(tag) { + tag = angular.lowercase(tag); + if (!ignore && validElements[tag] === true) { + out(''); + } + if (tag == ignore) { + ignore = false; + } + }, + chars: function(chars) { + if (!ignore) { + out(encodeEntities(chars)); + } + } + }; +} + + +// define ngSanitize module and register $sanitize service +angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); + +/* global sanitizeText: false */ + +/** + * @ngdoc filter + * @name linky + * @kind function + * + * @description + * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and + * plain email address links. + * + * Requires the {@link ngSanitize `ngSanitize`} module to be installed. + * + * @param {string} text Input text. + * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in. + * @returns {string} Html-linkified text. + * + * @usage + + * + * @example + + + +
+ Snippet: + + + + + + + + + + + + + + + + + + + + + +
FilterSourceRendered
linky filter +
<div ng-bind-html="snippet | linky">
</div>
+
+
+
linky target +
<div ng-bind-html="snippetWithTarget | linky:'_blank'">
</div>
+
+
+
no filter
<div ng-bind="snippet">
</div>
+ + + it('should linkify the snippet with urls', function() { + expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). + toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' + + 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); + expect(element.all(by.css('#linky-filter a')).count()).toEqual(4); + }); + + it('should not linkify snippet without the linky filter', function() { + expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()). + toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' + + 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); + expect(element.all(by.css('#escaped-html a')).count()).toEqual(0); + }); + + it('should update', function() { + element(by.model('snippet')).clear(); + element(by.model('snippet')).sendKeys('new http://link.'); + expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). + toBe('new http://link.'); + expect(element.all(by.css('#linky-filter a')).count()).toEqual(1); + expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()) + .toBe('new http://link.'); + }); + + it('should work with the target property', function() { + expect(element(by.id('linky-target')). + element(by.binding("snippetWithTarget | linky:'_blank'")).getText()). + toBe('http://angularjs.org/'); + expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank'); + }); + + + */ +angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { + var LINKY_URL_REGEXP = + /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"”’]/, + MAILTO_REGEXP = /^mailto:/; + + return function(text, target) { + if (!text) return text; + var match; + var raw = text; + var html = []; + var url; + var i; + while ((match = raw.match(LINKY_URL_REGEXP))) { + // We can not end in these as they are sometimes found at the end of the sentence + url = match[0]; + // if we did not match ftp/http/www/mailto then assume mailto + if (!match[2] && !match[4]) { + url = (match[3] ? 'http://' : 'mailto:') + url; + } + i = match.index; + addText(raw.substr(0, i)); + addLink(url, match[0].replace(MAILTO_REGEXP, '')); + raw = raw.substring(i + match[0].length); + } + addText(raw); + return $sanitize(html.join('')); + + function addText(text) { + if (!text) { + return; + } + html.push(sanitizeText(text)); + } + + function addLink(url, text) { + html.push(''); + addText(text); + html.push(''); + } + }; +}]); + + +})(window, window.angular); diff --git a/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js b/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js new file mode 100644 index 0000000000..f57a22b7f8 --- /dev/null +++ b/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js @@ -0,0 +1,19 @@ +describe('Security concerns', function() { + var highlightFilter, $sanitize; + + beforeEach(module('ui.bootstrap.typeahead', 'ngSanitize')); + + beforeEach(inject(function (typeaheadHighlightFilter, _$sanitize_) { + highlightFilter = typeaheadHighlightFilter; + $sanitize = _$sanitize_; + })); + + it('should escape the "script" tag when present', function() { + expect(highlightFilter('before match after', 'match')).toEqual('before match after'); + }); + + it('should escape attributes that execute javascript from html elements', function() { + expect(highlightFilter('before
match after', 'match')).toEqual('before
match after'); + }); + +}); \ No newline at end of file diff --git a/src/typeahead/test/typeahead-highlight.spec.js b/src/typeahead/test/typeahead-highlight.spec.js index 1a5aafc48d..1758b557f1 100644 --- a/src/typeahead/test/typeahead-highlight.spec.js +++ b/src/typeahead/test/typeahead-highlight.spec.js @@ -1,39 +1,50 @@ describe('typeaheadHighlight', function () { - var highlightFilter; + var highlightFilter, $log, logSpy; beforeEach(module('ui.bootstrap.typeahead')); + + beforeEach(inject(function(_$log_) { + $log = _$log_; + logSpy = spyOn($log, 'warn'); + })); + beforeEach(inject(function (typeaheadHighlightFilter) { highlightFilter = typeaheadHighlightFilter; })); it('should higlight a match', function () { - expect(highlightFilter('before match after', 'match')).toEqual('before match after'); + expect(highlightFilter('before match after', 'match').$$unwrapTrustedValue()).toEqual('before match after'); }); it('should higlight a match with mixed case', function () { - expect(highlightFilter('before MaTch after', 'match')).toEqual('before MaTch after'); + expect(highlightFilter('before MaTch after', 'match').$$unwrapTrustedValue()).toEqual('before MaTch after'); }); it('should higlight all matches', function () { - expect(highlightFilter('before MaTch after match', 'match')).toEqual('before MaTch after match'); + expect(highlightFilter('before MaTch after match', 'match').$$unwrapTrustedValue()).toEqual('before MaTch after match'); }); it('should do nothing if no match', function () { - expect(highlightFilter('before match after', 'nomatch')).toEqual('before match after'); + expect(highlightFilter('before match after', 'nomatch').$$unwrapTrustedValue()).toEqual('before match after'); }); it('should do nothing if no or empty query', function () { - expect(highlightFilter('before match after', '')).toEqual('before match after'); - expect(highlightFilter('before match after', null)).toEqual('before match after'); - expect(highlightFilter('before match after', undefined)).toEqual('before match after'); + expect(highlightFilter('before match after', '').$$unwrapTrustedValue()).toEqual('before match after'); + expect(highlightFilter('before match after', null).$$unwrapTrustedValue()).toEqual('before match after'); + expect(highlightFilter('before match after', undefined).$$unwrapTrustedValue()).toEqual('before match after'); }); it('issue 316 - should work correctly for regexp reserved words', function () { - expect(highlightFilter('before (match after', '(match')).toEqual('before (match after'); + expect(highlightFilter('before (match after', '(match').$$unwrapTrustedValue()).toEqual('before (match after'); }); it('issue 1777 - should work correctly with numeric values', function () { - expect(highlightFilter(123, '2')).toEqual('123'); + expect(highlightFilter(123, '2').$$unwrapTrustedValue()).toEqual('123'); }); + + it('should show a warning when this component is being used unsafely', function() { + expect(logSpy).toHaveBeenCalled(); + }); + }); diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index af786e010a..4a5d7caefc 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -399,13 +399,30 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap }; }]) - .filter('typeaheadHighlight', function() { + .filter('typeaheadHighlight', ['$sce', '$injector', '$log', function($sce, $injector, $log) { + + var $sanitize; + try { + $sanitize = $injector.get('$sanitize'); //tries to inject the sanitize service + } + catch(e) { + $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger + } function escapeRegexp(queryToEscape) { - return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); + return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); // Regex: capture the whole query string and replace it with the string that will be used to match the results, for example if the capture is "a" the result will be \a } return function(matchItem, query) { - return query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; + if($sanitize) { // Is the $sanitize service available? + if(/<.*>/g.test(matchItem)){ + matchItem = $sanitize(matchItem); // If it's present the string gets sanitized and packed in a $sce object. + } + } + matchItem = query? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag + if(!$sanitize) { + matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive + } + return matchItem; }; - }); + }]); diff --git a/template/typeahead/typeahead-match.html b/template/typeahead/typeahead-match.html index d79e10a18f..989ce8a78a 100644 --- a/template/typeahead/typeahead-match.html +++ b/template/typeahead/typeahead-match.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From 21a25dc99af02ab56659338ca18b007ba1f92e7e Mon Sep 17 00:00:00 2001 From: Fernando Ruiz Date: Thu, 2 Apr 2015 18:52:16 -0700 Subject: [PATCH 02/14] fix(typeahead): Refactor of previous fix --- src/typeahead/typeahead.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 4a5d7caefc..159c66375e 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -410,17 +410,16 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap } function escapeRegexp(queryToEscape) { - return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); // Regex: capture the whole query string and replace it with the string that will be used to match the results, for example if the capture is "a" the result will be \a + // Regex: capture the whole query string and replace it with the string that will be used to match + // the results, for example if the capture is "a" the result will be \a + return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); } return function(matchItem, query) { - if($sanitize) { // Is the $sanitize service available? - if(/<.*>/g.test(matchItem)){ - matchItem = $sanitize(matchItem); // If it's present the string gets sanitized and packed in a $sce object. - } - } matchItem = query? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag - if(!$sanitize) { + if($sanitize) { // Is the $sanitize service available? + matchItem = $sanitize(matchItem); // If it's present the string gets sanitized and packed in a $sce object. + } else { matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive } return matchItem; From 12b8b8d5e68509db7e113c4c90f636ff2a25040f Mon Sep 17 00:00:00 2001 From: Fernando Ruiz Date: Thu, 2 Apr 2015 18:53:22 -0700 Subject: [PATCH 03/14] fix(typeahead): Refactor of previous fix --- .../test/typeahead-highlight.spec.js | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/typeahead/test/typeahead-highlight.spec.js b/src/typeahead/test/typeahead-highlight.spec.js index 1758b557f1..91ae965f4a 100644 --- a/src/typeahead/test/typeahead-highlight.spec.js +++ b/src/typeahead/test/typeahead-highlight.spec.js @@ -1,11 +1,12 @@ describe('typeaheadHighlight', function () { - var highlightFilter, $log, logSpy; + var highlightFilter, $log, $sce, logSpy; beforeEach(module('ui.bootstrap.typeahead')); - beforeEach(inject(function(_$log_) { + beforeEach(inject(function(_$log_, _$sce_) { $log = _$log_; + $sce = _$sce_; logSpy = spyOn($log, 'warn'); })); @@ -14,33 +15,33 @@ describe('typeaheadHighlight', function () { })); it('should higlight a match', function () { - expect(highlightFilter('before match after', 'match').$$unwrapTrustedValue()).toEqual('before match after'); + expect($sce.getTrustedHtml(highlightFilter('before match after', 'match'))).toEqual('before match after'); }); it('should higlight a match with mixed case', function () { - expect(highlightFilter('before MaTch after', 'match').$$unwrapTrustedValue()).toEqual('before MaTch after'); + expect($sce.getTrustedHtml(highlightFilter('before MaTch after', 'match'))).toEqual('before MaTch after'); }); it('should higlight all matches', function () { - expect(highlightFilter('before MaTch after match', 'match').$$unwrapTrustedValue()).toEqual('before MaTch after match'); + expect($sce.getTrustedHtml(highlightFilter('before MaTch after match', 'match'))).toEqual('before MaTch after match'); }); it('should do nothing if no match', function () { - expect(highlightFilter('before match after', 'nomatch').$$unwrapTrustedValue()).toEqual('before match after'); + expect($sce.getTrustedHtml(highlightFilter('before match after', 'nomatch'))).toEqual('before match after'); }); it('should do nothing if no or empty query', function () { - expect(highlightFilter('before match after', '').$$unwrapTrustedValue()).toEqual('before match after'); - expect(highlightFilter('before match after', null).$$unwrapTrustedValue()).toEqual('before match after'); - expect(highlightFilter('before match after', undefined).$$unwrapTrustedValue()).toEqual('before match after'); + expect($sce.getTrustedHtml(highlightFilter('before match after', ''))).toEqual('before match after'); + expect($sce.getTrustedHtml(highlightFilter('before match after', null))).toEqual('before match after'); + expect($sce.getTrustedHtml(highlightFilter('before match after', undefined))).toEqual('before match after'); }); it('issue 316 - should work correctly for regexp reserved words', function () { - expect(highlightFilter('before (match after', '(match').$$unwrapTrustedValue()).toEqual('before (match after'); + expect($sce.getTrustedHtml(highlightFilter('before (match after', '(match'))).toEqual('before (match after'); }); it('issue 1777 - should work correctly with numeric values', function () { - expect(highlightFilter(123, '2').$$unwrapTrustedValue()).toEqual('123'); + expect($sce.getTrustedHtml(highlightFilter(123, '2'))).toEqual('123'); }); it('should show a warning when this component is being used unsafely', function() { From ab80e9b83f698a2233daf6d5e5e90c3d00d2c2f1 Mon Sep 17 00:00:00 2001 From: Fernando Ruiz Date: Thu, 9 Apr 2015 20:18:17 -0700 Subject: [PATCH 04/14] fix(typeahead): Changes to typeaheadHighlight --- misc/demo/assets/app.js | 2 +- package.json | 1 + .../test/typeahead-highlight-ngsanitize.spec.js | 16 ++++++++-------- src/typeahead/test/typeahead-highlight.spec.js | 1 + src/typeahead/typeahead.js | 13 +++++++++---- 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/misc/demo/assets/app.js b/misc/demo/assets/app.js index af0c4a8ff1..8d19f5481b 100644 --- a/misc/demo/assets/app.js +++ b/misc/demo/assets/app.js @@ -1,5 +1,5 @@ /* global FastClick, smoothScroll */ -angular.module('ui.bootstrap.demo', ['ui.bootstrap', 'plunker', 'ngTouch', 'ngAnimate'], function($httpProvider){ +angular.module('ui.bootstrap.demo', ['ui.bootstrap', 'plunker', 'ngTouch', 'ngAnimate', 'ngSanitize'], function($httpProvider){ FastClick.attach(document.body); delete $httpProvider.defaults.headers.common['X-Requested-With']; }).run(['$location', function($location){ diff --git a/package.json b/package.json index a02bc584b4..262e0d4bb6 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "devDependencies": { "angular": "^1.3.15", "angular-mocks": "^1.3.15", + "angular-sanitize": "^1.3.15", "grunt": "^0.4.5", "grunt-contrib-concat": "^0.5.1", "grunt-contrib-copy": "^0.8.0", diff --git a/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js b/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js index f57a22b7f8..b659baad2a 100644 --- a/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js +++ b/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js @@ -1,19 +1,19 @@ describe('Security concerns', function() { - var highlightFilter, $sanitize; + var highlightFilter, + $sanitize, + logSpy; beforeEach(module('ui.bootstrap.typeahead', 'ngSanitize')); - beforeEach(inject(function (typeaheadHighlightFilter, _$sanitize_) { + beforeEach(inject(function (typeaheadHighlightFilter, _$sanitize_, $log) { highlightFilter = typeaheadHighlightFilter; $sanitize = _$sanitize_; + logSpy = spyOn($log, 'warn'); })); - it('should escape the "script" tag when present', function() { - expect(highlightFilter('before match after', 'match')).toEqual('before match after'); - }); - - it('should escape attributes that execute javascript from html elements', function() { - expect(highlightFilter('before
match after', 'match')).toEqual('before
match after'); + it('should not call the $log service when ngSanitize is present', function() { + highlightFilter('before after', 'match'); + expect(logSpy).not.toHaveBeenCalled(); }); }); \ No newline at end of file diff --git a/src/typeahead/test/typeahead-highlight.spec.js b/src/typeahead/test/typeahead-highlight.spec.js index 91ae965f4a..9ed3020904 100644 --- a/src/typeahead/test/typeahead-highlight.spec.js +++ b/src/typeahead/test/typeahead-highlight.spec.js @@ -45,6 +45,7 @@ describe('typeaheadHighlight', function () { }); it('should show a warning when this component is being used unsafely', function() { + highlightFilter('before match after', 'match'); expect(logSpy).toHaveBeenCalled(); }); diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 159c66375e..60c155f0b5 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -406,7 +406,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap $sanitize = $injector.get('$sanitize'); //tries to inject the sanitize service } catch(e) { - $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger + // Catches the exception when the $sanitize service is not available. } function escapeRegexp(queryToEscape) { @@ -415,11 +415,16 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); } + function containsHtml(matchItem) { + return /<.*>/g.test(matchItem); + } + return function(matchItem, query) { + if(!$sanitize && containsHtml(matchItem)) { + $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger + } matchItem = query? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag - if($sanitize) { // Is the $sanitize service available? - matchItem = $sanitize(matchItem); // If it's present the string gets sanitized and packed in a $sce object. - } else { + if(!$sanitize) { matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive } return matchItem; From 46312197dc7f8abc1dff4d20f50460dfa0b71352 Mon Sep 17 00:00:00 2001 From: Fernando Ruiz Date: Thu, 9 Apr 2015 20:23:50 -0700 Subject: [PATCH 05/14] fix(typeahead): Removed dependency ui.bootstrap.bindHtml --- src/typeahead/typeahead.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 60c155f0b5..824d4998d9 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -1,4 +1,4 @@ -angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml']) +angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) /** * A helper service that can parse typeahead's syntax (string provided by users) From d38dfcc5331eeaad3d1d341b4a66d9a048965897 Mon Sep 17 00:00:00 2001 From: Fernando Ruiz Date: Mon, 13 Apr 2015 19:04:25 -0700 Subject: [PATCH 06/14] fix(typeahead): Refactored code --- karma.conf.js | 2 +- misc/test-lib/angular-sanitize.js | 681 ------------------------------ src/typeahead/typeahead.js | 13 +- 3 files changed, 5 insertions(+), 691 deletions(-) delete mode 100644 misc/test-lib/angular-sanitize.js diff --git a/karma.conf.js b/karma.conf.js index 8f4f63e0c2..34e03d39c7 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -18,7 +18,7 @@ module.exports = function(config) { 'misc/test-lib/jquery-1.8.2.min.js', 'node_modules/angular/angular.js', 'node_modules/angular-mocks/angular-mocks.js', - 'misc/test-lib/angular-sanitize.js', + 'node_modules/angular-sanitize/angular-sanitize.js', 'misc/test-lib/helpers.js', 'src/**/*.js', 'template/**/*.js' diff --git a/misc/test-lib/angular-sanitize.js b/misc/test-lib/angular-sanitize.js deleted file mode 100644 index b629a52433..0000000000 --- a/misc/test-lib/angular-sanitize.js +++ /dev/null @@ -1,681 +0,0 @@ -/** - * @license AngularJS v1.3.13 - * (c) 2010-2014 Google, Inc. http://angularjs.org - * License: MIT - */ -(function(window, angular, undefined) {'use strict'; - -var $sanitizeMinErr = angular.$$minErr('$sanitize'); - -/** - * @ngdoc module - * @name ngSanitize - * @description - * - * # ngSanitize - * - * The `ngSanitize` module provides functionality to sanitize HTML. - * - * - *
- * - * See {@link ngSanitize.$sanitize `$sanitize`} for usage. - */ - -/* - * HTML Parser By Misko Hevery (misko@hevery.com) - * based on: HTML Parser By John Resig (ejohn.org) - * Original code by Erik Arvidsson, Mozilla Public License - * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js - * - * // Use like so: - * htmlParser(htmlString, { - * start: function(tag, attrs, unary) {}, - * end: function(tag) {}, - * chars: function(text) {}, - * comment: function(text) {} - * }); - * - */ - - -/** - * @ngdoc service - * @name $sanitize - * @kind function - * - * @description - * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are - * then serialized back to properly escaped html string. This means that no unsafe input can make - * it into the returned string, however, since our parser is more strict than a typical browser - * parser, it's possible that some obscure input, which would be recognized as valid HTML by a - * browser, won't make it through the sanitizer. The input may also contain SVG markup. - * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and - * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}. - * - * @param {string} html HTML input. - * @returns {string} Sanitized HTML. - * - * @example - - - -
- Snippet: - - - - - - - - - - - - - - - - - - - - - - - - - -
DirectiveHowSourceRendered
ng-bind-htmlAutomatically uses $sanitize
<div ng-bind-html="snippet">
</div>
ng-bind-htmlBypass $sanitize by explicitly trusting the dangerous value -
<div ng-bind-html="deliberatelyTrustDangerousSnippet()">
-</div>
-
ng-bindAutomatically escapes
<div ng-bind="snippet">
</div>
-
-
- - it('should sanitize the html snippet by default', function() { - expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). - toBe('

an html\nclick here\nsnippet

'); - }); - - it('should inline raw snippet if bound to a trusted value', function() { - expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()). - toBe("

an html\n" + - "click here\n" + - "snippet

"); - }); - - it('should escape snippet without any filter', function() { - expect(element(by.css('#bind-default div')).getInnerHtml()). - toBe("<p style=\"color:blue\">an html\n" + - "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + - "snippet</p>"); - }); - - it('should update', function() { - element(by.model('snippet')).clear(); - element(by.model('snippet')).sendKeys('new text'); - expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). - toBe('new text'); - expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe( - 'new text'); - expect(element(by.css('#bind-default div')).getInnerHtml()).toBe( - "new <b onclick=\"alert(1)\">text</b>"); - }); -
-
- */ -function $SanitizeProvider() { - this.$get = ['$$sanitizeUri', function($$sanitizeUri) { - return function(html) { - var buf = []; - htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) { - return !/^unsafe/.test($$sanitizeUri(uri, isImage)); - })); - return buf.join(''); - }; - }]; -} - -function sanitizeText(chars) { - var buf = []; - var writer = htmlSanitizeWriter(buf, angular.noop); - writer.chars(chars); - return buf.join(''); -} - - -// Regular Expressions for parsing tags and attributes -var START_TAG_REGEXP = - /^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/, - END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/, - ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, - BEGIN_TAG_REGEXP = /^/g, - DOCTYPE_REGEXP = /]*?)>/i, - CDATA_REGEXP = //g, - SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, - // Match everything outside of normal chars and " (quote character) - NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; - - -// Good source of info about elements and attributes -// http://dev.w3.org/html5/spec/Overview.html#semantics -// http://simon.html5.org/html-elements - -// Safe Void Elements - HTML5 -// http://dev.w3.org/html5/spec/Overview.html#void-elements -var voidElements = makeMap("area,br,col,hr,img,wbr"); - -// Elements that you can, intentionally, leave open (and which close themselves) -// http://dev.w3.org/html5/spec/Overview.html#optional-tags -var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), - optionalEndTagInlineElements = makeMap("rp,rt"), - optionalEndTagElements = angular.extend({}, - optionalEndTagInlineElements, - optionalEndTagBlockElements); - -// Safe Block Elements - HTML5 -var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," + - "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," + - "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); - -// Inline Elements - HTML5 -var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," + - "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," + - "samp,small,span,strike,strong,sub,sup,time,tt,u,var")); - -// SVG Elements -// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements -var svgElements = makeMap("animate,animateColor,animateMotion,animateTransform,circle,defs," + - "desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,hkern,image,linearGradient," + - "line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,radialGradient,rect,set," + - "stop,svg,switch,text,title,tspan,use"); - -// Special Elements (can contain anything) -var specialElements = makeMap("script,style"); - -var validElements = angular.extend({}, - voidElements, - blockElements, - inlineElements, - optionalEndTagElements, - svgElements); - -//Attributes that have href and hence need to be sanitized -var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap,xlink:href"); - -var htmlAttrs = makeMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' + - 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' + - 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' + - 'scope,scrolling,shape,size,span,start,summary,target,title,type,' + - 'valign,value,vspace,width'); - -// SVG attributes (without "id" and "name" attributes) -// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes -var svgAttrs = makeMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' + - 'attributeName,attributeType,baseProfile,bbox,begin,by,calcMode,cap-height,class,color,' + - 'color-rendering,content,cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,' + - 'font-size,font-stretch,font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,' + - 'gradientUnits,hanging,height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,' + - 'keySplines,keyTimes,lang,marker-end,marker-mid,marker-start,markerHeight,markerUnits,' + - 'markerWidth,mathematical,max,min,offset,opacity,orient,origin,overline-position,' + - 'overline-thickness,panose-1,path,pathLength,points,preserveAspectRatio,r,refX,refY,' + - 'repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,' + - 'stemv,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke,' + - 'stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,' + - 'stroke-opacity,stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,' + - 'underline-position,underline-thickness,unicode,unicode-range,units-per-em,values,version,' + - 'viewBox,visibility,width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,' + - 'xlink:show,xlink:title,xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,' + - 'zoomAndPan'); - -var validAttrs = angular.extend({}, - uriAttrs, - svgAttrs, - htmlAttrs); - -function makeMap(str) { - var obj = {}, items = str.split(','), i; - for (i = 0; i < items.length; i++) obj[items[i]] = true; - return obj; -} - - -/** - * @example - * htmlParser(htmlString, { - * start: function(tag, attrs, unary) {}, - * end: function(tag) {}, - * chars: function(text) {}, - * comment: function(text) {} - * }); - * - * @param {string} html string - * @param {object} handler - */ -function htmlParser(html, handler) { - if (typeof html !== 'string') { - if (html === null || typeof html === 'undefined') { - html = ''; - } else { - html = '' + html; - } - } - var index, chars, match, stack = [], last = html, text; - stack.last = function() { return stack[stack.length - 1]; }; - - while (html) { - text = ''; - chars = true; - - // Make sure we're not in a script or style element - if (!stack.last() || !specialElements[stack.last()]) { - - // Comment - if (html.indexOf("", index) === index) { - if (handler.comment) handler.comment(html.substring(4, index)); - html = html.substring(index + 3); - chars = false; - } - // DOCTYPE - } else if (DOCTYPE_REGEXP.test(html)) { - match = html.match(DOCTYPE_REGEXP); - - if (match) { - html = html.replace(match[0], ''); - chars = false; - } - // end tag - } else if (BEGING_END_TAGE_REGEXP.test(html)) { - match = html.match(END_TAG_REGEXP); - - if (match) { - html = html.substring(match[0].length); - match[0].replace(END_TAG_REGEXP, parseEndTag); - chars = false; - } - - // start tag - } else if (BEGIN_TAG_REGEXP.test(html)) { - match = html.match(START_TAG_REGEXP); - - if (match) { - // We only have a valid start-tag if there is a '>'. - if (match[4]) { - html = html.substring(match[0].length); - match[0].replace(START_TAG_REGEXP, parseStartTag); - } - chars = false; - } else { - // no ending tag found --- this piece should be encoded as an entity. - text += '<'; - html = html.substring(1); - } - } - - if (chars) { - index = html.indexOf("<"); - - text += index < 0 ? html : html.substring(0, index); - html = index < 0 ? "" : html.substring(index); - - if (handler.chars) handler.chars(decodeEntities(text)); - } - - } else { - // IE versions 9 and 10 do not understand the regex '[^]', so using a workaround with [\W\w]. - html = html.replace(new RegExp("([\\W\\w]*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), - function(all, text) { - text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1"); - - if (handler.chars) handler.chars(decodeEntities(text)); - - return ""; - }); - - parseEndTag("", stack.last()); - } - - if (html == last) { - throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " + - "of html: {0}", html); - } - last = html; - } - - // Clean up any remaining tags - parseEndTag(); - - function parseStartTag(tag, tagName, rest, unary) { - tagName = angular.lowercase(tagName); - if (blockElements[tagName]) { - while (stack.last() && inlineElements[stack.last()]) { - parseEndTag("", stack.last()); - } - } - - if (optionalEndTagElements[tagName] && stack.last() == tagName) { - parseEndTag("", tagName); - } - - unary = voidElements[tagName] || !!unary; - - if (!unary) - stack.push(tagName); - - var attrs = {}; - - rest.replace(ATTR_REGEXP, - function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) { - var value = doubleQuotedValue - || singleQuotedValue - || unquotedValue - || ''; - - attrs[name] = decodeEntities(value); - }); - if (handler.start) handler.start(tagName, attrs, unary); - } - - function parseEndTag(tag, tagName) { - var pos = 0, i; - tagName = angular.lowercase(tagName); - if (tagName) - // Find the closest opened tag of the same type - for (pos = stack.length - 1; pos >= 0; pos--) - if (stack[pos] == tagName) - break; - - if (pos >= 0) { - // Close all the open elements, up the stack - for (i = stack.length - 1; i >= pos; i--) - if (handler.end) handler.end(stack[i]); - - // Remove the open elements from the stack - stack.length = pos; - } - } -} - -var hiddenPre=document.createElement("pre"); -var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/; -/** - * decodes all entities into regular string - * @param value - * @returns {string} A string with decoded entities. - */ -function decodeEntities(value) { - if (!value) { return ''; } - - // Note: IE8 does not preserve spaces at the start/end of innerHTML - // so we must capture them and reattach them afterward - var parts = spaceRe.exec(value); - var spaceBefore = parts[1]; - var spaceAfter = parts[3]; - var content = parts[2]; - if (content) { - hiddenPre.innerHTML=content.replace(//g, '>'); -} - -/** - * create an HTML/XML writer which writes to buffer - * @param {Array} buf use buf.jain('') to get out sanitized html string - * @returns {object} in the form of { - * start: function(tag, attrs, unary) {}, - * end: function(tag) {}, - * chars: function(text) {}, - * comment: function(text) {} - * } - */ -function htmlSanitizeWriter(buf, uriValidator) { - var ignore = false; - var out = angular.bind(buf, buf.push); - return { - start: function(tag, attrs, unary) { - tag = angular.lowercase(tag); - if (!ignore && specialElements[tag]) { - ignore = tag; - } - if (!ignore && validElements[tag] === true) { - out('<'); - out(tag); - angular.forEach(attrs, function(value, key) { - var lkey=angular.lowercase(key); - var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background'); - if (validAttrs[lkey] === true && - (uriAttrs[lkey] !== true || uriValidator(value, isImage))) { - out(' '); - out(key); - out('="'); - out(encodeEntities(value)); - out('"'); - } - }); - out(unary ? '/>' : '>'); - } - }, - end: function(tag) { - tag = angular.lowercase(tag); - if (!ignore && validElements[tag] === true) { - out(''); - } - if (tag == ignore) { - ignore = false; - } - }, - chars: function(chars) { - if (!ignore) { - out(encodeEntities(chars)); - } - } - }; -} - - -// define ngSanitize module and register $sanitize service -angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); - -/* global sanitizeText: false */ - -/** - * @ngdoc filter - * @name linky - * @kind function - * - * @description - * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and - * plain email address links. - * - * Requires the {@link ngSanitize `ngSanitize`} module to be installed. - * - * @param {string} text Input text. - * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in. - * @returns {string} Html-linkified text. - * - * @usage - - * - * @example - - - -
- Snippet: - - - - - - - - - - - - - - - - - - - - - -
FilterSourceRendered
linky filter -
<div ng-bind-html="snippet | linky">
</div>
-
-
-
linky target -
<div ng-bind-html="snippetWithTarget | linky:'_blank'">
</div>
-
-
-
no filter
<div ng-bind="snippet">
</div>
- - - it('should linkify the snippet with urls', function() { - expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). - toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' + - 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); - expect(element.all(by.css('#linky-filter a')).count()).toEqual(4); - }); - - it('should not linkify snippet without the linky filter', function() { - expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()). - toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' + - 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); - expect(element.all(by.css('#escaped-html a')).count()).toEqual(0); - }); - - it('should update', function() { - element(by.model('snippet')).clear(); - element(by.model('snippet')).sendKeys('new http://link.'); - expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). - toBe('new http://link.'); - expect(element.all(by.css('#linky-filter a')).count()).toEqual(1); - expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()) - .toBe('new http://link.'); - }); - - it('should work with the target property', function() { - expect(element(by.id('linky-target')). - element(by.binding("snippetWithTarget | linky:'_blank'")).getText()). - toBe('http://angularjs.org/'); - expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank'); - }); - - - */ -angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { - var LINKY_URL_REGEXP = - /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"”’]/, - MAILTO_REGEXP = /^mailto:/; - - return function(text, target) { - if (!text) return text; - var match; - var raw = text; - var html = []; - var url; - var i; - while ((match = raw.match(LINKY_URL_REGEXP))) { - // We can not end in these as they are sometimes found at the end of the sentence - url = match[0]; - // if we did not match ftp/http/www/mailto then assume mailto - if (!match[2] && !match[4]) { - url = (match[3] ? 'http://' : 'mailto:') + url; - } - i = match.index; - addText(raw.substr(0, i)); - addLink(url, match[0].replace(MAILTO_REGEXP, '')); - raw = raw.substring(i + match[0].length); - } - addText(raw); - return $sanitize(html.join('')); - - function addText(text) { - if (!text) { - return; - } - html.push(sanitizeText(text)); - } - - function addLink(url, text) { - html.push(''); - addText(text); - html.push(''); - } - }; -}]); - - -})(window, window.angular); diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 824d4998d9..c4c2dc0283 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -401,13 +401,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) .filter('typeaheadHighlight', ['$sce', '$injector', '$log', function($sce, $injector, $log) { - var $sanitize; - try { - $sanitize = $injector.get('$sanitize'); //tries to inject the sanitize service - } - catch(e) { - // Catches the exception when the $sanitize service is not available. - } + var isSanitizePresent; + isSanitizePresent = $injector.has('$sanitize'); function escapeRegexp(queryToEscape) { // Regex: capture the whole query string and replace it with the string that will be used to match @@ -420,11 +415,11 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) } return function(matchItem, query) { - if(!$sanitize && containsHtml(matchItem)) { + if(!isSanitizePresent && containsHtml(matchItem)) { $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger } matchItem = query? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag - if(!$sanitize) { + if(!isSanitizePresent) { matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive } return matchItem; From 69246f80cae9726e53850be73b850143a7dd4a73 Mon Sep 17 00:00:00 2001 From: Fernando Ruiz Date: Tue, 14 Apr 2015 19:12:49 -0700 Subject: [PATCH 07/14] fix(typeahead): Code groom. --- src/typeahead/test/typeahead-highlight-ngsanitize.spec.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js b/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js index b659baad2a..17c445528a 100644 --- a/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js +++ b/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js @@ -1,7 +1,5 @@ describe('Security concerns', function() { - var highlightFilter, - $sanitize, - logSpy; + var highlightFilter, $sanitize, logSpy; beforeEach(module('ui.bootstrap.typeahead', 'ngSanitize')); From 4db9a670f4010a3894dd760c3b8561cb044cb4c3 Mon Sep 17 00:00:00 2001 From: FernandoSendMail Date: Sun, 29 Mar 2015 02:08:36 -0700 Subject: [PATCH 08/14] fix(typeahead): Typeahead uses ngSanitize when present --- misc/demo/index.html | 1 + misc/test-lib/angular-sanitize.js | 681 ++++++++++++++++++ .../typeahead-highlight-ngsanitize.spec.js | 19 + .../test/typeahead-highlight.spec.js | 31 +- src/typeahead/typeahead.js | 25 +- 5 files changed, 743 insertions(+), 14 deletions(-) create mode 100644 misc/test-lib/angular-sanitize.js create mode 100644 src/typeahead/test/typeahead-highlight-ngsanitize.spec.js diff --git a/misc/demo/index.html b/misc/demo/index.html index aa5f412e26..358bcbf0d4 100644 --- a/misc/demo/index.html +++ b/misc/demo/index.html @@ -14,6 +14,7 @@ + diff --git a/misc/test-lib/angular-sanitize.js b/misc/test-lib/angular-sanitize.js new file mode 100644 index 0000000000..b629a52433 --- /dev/null +++ b/misc/test-lib/angular-sanitize.js @@ -0,0 +1,681 @@ +/** + * @license AngularJS v1.3.13 + * (c) 2010-2014 Google, Inc. http://angularjs.org + * License: MIT + */ +(function(window, angular, undefined) {'use strict'; + +var $sanitizeMinErr = angular.$$minErr('$sanitize'); + +/** + * @ngdoc module + * @name ngSanitize + * @description + * + * # ngSanitize + * + * The `ngSanitize` module provides functionality to sanitize HTML. + * + * + *
+ * + * See {@link ngSanitize.$sanitize `$sanitize`} for usage. + */ + +/* + * HTML Parser By Misko Hevery (misko@hevery.com) + * based on: HTML Parser By John Resig (ejohn.org) + * Original code by Erik Arvidsson, Mozilla Public License + * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js + * + * // Use like so: + * htmlParser(htmlString, { + * start: function(tag, attrs, unary) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * }); + * + */ + + +/** + * @ngdoc service + * @name $sanitize + * @kind function + * + * @description + * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are + * then serialized back to properly escaped html string. This means that no unsafe input can make + * it into the returned string, however, since our parser is more strict than a typical browser + * parser, it's possible that some obscure input, which would be recognized as valid HTML by a + * browser, won't make it through the sanitizer. The input may also contain SVG markup. + * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and + * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}. + * + * @param {string} html HTML input. + * @returns {string} Sanitized HTML. + * + * @example + + + +
+ Snippet: + + + + + + + + + + + + + + + + + + + + + + + + + +
DirectiveHowSourceRendered
ng-bind-htmlAutomatically uses $sanitize
<div ng-bind-html="snippet">
</div>
ng-bind-htmlBypass $sanitize by explicitly trusting the dangerous value +
<div ng-bind-html="deliberatelyTrustDangerousSnippet()">
+</div>
+
ng-bindAutomatically escapes
<div ng-bind="snippet">
</div>
+
+
+ + it('should sanitize the html snippet by default', function() { + expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). + toBe('

an html\nclick here\nsnippet

'); + }); + + it('should inline raw snippet if bound to a trusted value', function() { + expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()). + toBe("

an html\n" + + "click here\n" + + "snippet

"); + }); + + it('should escape snippet without any filter', function() { + expect(element(by.css('#bind-default div')).getInnerHtml()). + toBe("<p style=\"color:blue\">an html\n" + + "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + + "snippet</p>"); + }); + + it('should update', function() { + element(by.model('snippet')).clear(); + element(by.model('snippet')).sendKeys('new text'); + expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). + toBe('new text'); + expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe( + 'new text'); + expect(element(by.css('#bind-default div')).getInnerHtml()).toBe( + "new <b onclick=\"alert(1)\">text</b>"); + }); +
+
+ */ +function $SanitizeProvider() { + this.$get = ['$$sanitizeUri', function($$sanitizeUri) { + return function(html) { + var buf = []; + htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) { + return !/^unsafe/.test($$sanitizeUri(uri, isImage)); + })); + return buf.join(''); + }; + }]; +} + +function sanitizeText(chars) { + var buf = []; + var writer = htmlSanitizeWriter(buf, angular.noop); + writer.chars(chars); + return buf.join(''); +} + + +// Regular Expressions for parsing tags and attributes +var START_TAG_REGEXP = + /^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/, + END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/, + ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, + BEGIN_TAG_REGEXP = /^/g, + DOCTYPE_REGEXP = /]*?)>/i, + CDATA_REGEXP = //g, + SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, + // Match everything outside of normal chars and " (quote character) + NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; + + +// Good source of info about elements and attributes +// http://dev.w3.org/html5/spec/Overview.html#semantics +// http://simon.html5.org/html-elements + +// Safe Void Elements - HTML5 +// http://dev.w3.org/html5/spec/Overview.html#void-elements +var voidElements = makeMap("area,br,col,hr,img,wbr"); + +// Elements that you can, intentionally, leave open (and which close themselves) +// http://dev.w3.org/html5/spec/Overview.html#optional-tags +var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), + optionalEndTagInlineElements = makeMap("rp,rt"), + optionalEndTagElements = angular.extend({}, + optionalEndTagInlineElements, + optionalEndTagBlockElements); + +// Safe Block Elements - HTML5 +var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," + + "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," + + "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); + +// Inline Elements - HTML5 +var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," + + "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," + + "samp,small,span,strike,strong,sub,sup,time,tt,u,var")); + +// SVG Elements +// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements +var svgElements = makeMap("animate,animateColor,animateMotion,animateTransform,circle,defs," + + "desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,hkern,image,linearGradient," + + "line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,radialGradient,rect,set," + + "stop,svg,switch,text,title,tspan,use"); + +// Special Elements (can contain anything) +var specialElements = makeMap("script,style"); + +var validElements = angular.extend({}, + voidElements, + blockElements, + inlineElements, + optionalEndTagElements, + svgElements); + +//Attributes that have href and hence need to be sanitized +var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap,xlink:href"); + +var htmlAttrs = makeMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' + + 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' + + 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' + + 'scope,scrolling,shape,size,span,start,summary,target,title,type,' + + 'valign,value,vspace,width'); + +// SVG attributes (without "id" and "name" attributes) +// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes +var svgAttrs = makeMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' + + 'attributeName,attributeType,baseProfile,bbox,begin,by,calcMode,cap-height,class,color,' + + 'color-rendering,content,cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,' + + 'font-size,font-stretch,font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,' + + 'gradientUnits,hanging,height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,' + + 'keySplines,keyTimes,lang,marker-end,marker-mid,marker-start,markerHeight,markerUnits,' + + 'markerWidth,mathematical,max,min,offset,opacity,orient,origin,overline-position,' + + 'overline-thickness,panose-1,path,pathLength,points,preserveAspectRatio,r,refX,refY,' + + 'repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,' + + 'stemv,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke,' + + 'stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,' + + 'stroke-opacity,stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,' + + 'underline-position,underline-thickness,unicode,unicode-range,units-per-em,values,version,' + + 'viewBox,visibility,width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,' + + 'xlink:show,xlink:title,xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,' + + 'zoomAndPan'); + +var validAttrs = angular.extend({}, + uriAttrs, + svgAttrs, + htmlAttrs); + +function makeMap(str) { + var obj = {}, items = str.split(','), i; + for (i = 0; i < items.length; i++) obj[items[i]] = true; + return obj; +} + + +/** + * @example + * htmlParser(htmlString, { + * start: function(tag, attrs, unary) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * }); + * + * @param {string} html string + * @param {object} handler + */ +function htmlParser(html, handler) { + if (typeof html !== 'string') { + if (html === null || typeof html === 'undefined') { + html = ''; + } else { + html = '' + html; + } + } + var index, chars, match, stack = [], last = html, text; + stack.last = function() { return stack[stack.length - 1]; }; + + while (html) { + text = ''; + chars = true; + + // Make sure we're not in a script or style element + if (!stack.last() || !specialElements[stack.last()]) { + + // Comment + if (html.indexOf("", index) === index) { + if (handler.comment) handler.comment(html.substring(4, index)); + html = html.substring(index + 3); + chars = false; + } + // DOCTYPE + } else if (DOCTYPE_REGEXP.test(html)) { + match = html.match(DOCTYPE_REGEXP); + + if (match) { + html = html.replace(match[0], ''); + chars = false; + } + // end tag + } else if (BEGING_END_TAGE_REGEXP.test(html)) { + match = html.match(END_TAG_REGEXP); + + if (match) { + html = html.substring(match[0].length); + match[0].replace(END_TAG_REGEXP, parseEndTag); + chars = false; + } + + // start tag + } else if (BEGIN_TAG_REGEXP.test(html)) { + match = html.match(START_TAG_REGEXP); + + if (match) { + // We only have a valid start-tag if there is a '>'. + if (match[4]) { + html = html.substring(match[0].length); + match[0].replace(START_TAG_REGEXP, parseStartTag); + } + chars = false; + } else { + // no ending tag found --- this piece should be encoded as an entity. + text += '<'; + html = html.substring(1); + } + } + + if (chars) { + index = html.indexOf("<"); + + text += index < 0 ? html : html.substring(0, index); + html = index < 0 ? "" : html.substring(index); + + if (handler.chars) handler.chars(decodeEntities(text)); + } + + } else { + // IE versions 9 and 10 do not understand the regex '[^]', so using a workaround with [\W\w]. + html = html.replace(new RegExp("([\\W\\w]*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), + function(all, text) { + text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1"); + + if (handler.chars) handler.chars(decodeEntities(text)); + + return ""; + }); + + parseEndTag("", stack.last()); + } + + if (html == last) { + throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " + + "of html: {0}", html); + } + last = html; + } + + // Clean up any remaining tags + parseEndTag(); + + function parseStartTag(tag, tagName, rest, unary) { + tagName = angular.lowercase(tagName); + if (blockElements[tagName]) { + while (stack.last() && inlineElements[stack.last()]) { + parseEndTag("", stack.last()); + } + } + + if (optionalEndTagElements[tagName] && stack.last() == tagName) { + parseEndTag("", tagName); + } + + unary = voidElements[tagName] || !!unary; + + if (!unary) + stack.push(tagName); + + var attrs = {}; + + rest.replace(ATTR_REGEXP, + function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) { + var value = doubleQuotedValue + || singleQuotedValue + || unquotedValue + || ''; + + attrs[name] = decodeEntities(value); + }); + if (handler.start) handler.start(tagName, attrs, unary); + } + + function parseEndTag(tag, tagName) { + var pos = 0, i; + tagName = angular.lowercase(tagName); + if (tagName) + // Find the closest opened tag of the same type + for (pos = stack.length - 1; pos >= 0; pos--) + if (stack[pos] == tagName) + break; + + if (pos >= 0) { + // Close all the open elements, up the stack + for (i = stack.length - 1; i >= pos; i--) + if (handler.end) handler.end(stack[i]); + + // Remove the open elements from the stack + stack.length = pos; + } + } +} + +var hiddenPre=document.createElement("pre"); +var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/; +/** + * decodes all entities into regular string + * @param value + * @returns {string} A string with decoded entities. + */ +function decodeEntities(value) { + if (!value) { return ''; } + + // Note: IE8 does not preserve spaces at the start/end of innerHTML + // so we must capture them and reattach them afterward + var parts = spaceRe.exec(value); + var spaceBefore = parts[1]; + var spaceAfter = parts[3]; + var content = parts[2]; + if (content) { + hiddenPre.innerHTML=content.replace(//g, '>'); +} + +/** + * create an HTML/XML writer which writes to buffer + * @param {Array} buf use buf.jain('') to get out sanitized html string + * @returns {object} in the form of { + * start: function(tag, attrs, unary) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * } + */ +function htmlSanitizeWriter(buf, uriValidator) { + var ignore = false; + var out = angular.bind(buf, buf.push); + return { + start: function(tag, attrs, unary) { + tag = angular.lowercase(tag); + if (!ignore && specialElements[tag]) { + ignore = tag; + } + if (!ignore && validElements[tag] === true) { + out('<'); + out(tag); + angular.forEach(attrs, function(value, key) { + var lkey=angular.lowercase(key); + var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background'); + if (validAttrs[lkey] === true && + (uriAttrs[lkey] !== true || uriValidator(value, isImage))) { + out(' '); + out(key); + out('="'); + out(encodeEntities(value)); + out('"'); + } + }); + out(unary ? '/>' : '>'); + } + }, + end: function(tag) { + tag = angular.lowercase(tag); + if (!ignore && validElements[tag] === true) { + out(''); + } + if (tag == ignore) { + ignore = false; + } + }, + chars: function(chars) { + if (!ignore) { + out(encodeEntities(chars)); + } + } + }; +} + + +// define ngSanitize module and register $sanitize service +angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); + +/* global sanitizeText: false */ + +/** + * @ngdoc filter + * @name linky + * @kind function + * + * @description + * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and + * plain email address links. + * + * Requires the {@link ngSanitize `ngSanitize`} module to be installed. + * + * @param {string} text Input text. + * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in. + * @returns {string} Html-linkified text. + * + * @usage + + * + * @example + + + +
+ Snippet: + + + + + + + + + + + + + + + + + + + + + +
FilterSourceRendered
linky filter +
<div ng-bind-html="snippet | linky">
</div>
+
+
+
linky target +
<div ng-bind-html="snippetWithTarget | linky:'_blank'">
</div>
+
+
+
no filter
<div ng-bind="snippet">
</div>
+ + + it('should linkify the snippet with urls', function() { + expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). + toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' + + 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); + expect(element.all(by.css('#linky-filter a')).count()).toEqual(4); + }); + + it('should not linkify snippet without the linky filter', function() { + expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()). + toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' + + 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); + expect(element.all(by.css('#escaped-html a')).count()).toEqual(0); + }); + + it('should update', function() { + element(by.model('snippet')).clear(); + element(by.model('snippet')).sendKeys('new http://link.'); + expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). + toBe('new http://link.'); + expect(element.all(by.css('#linky-filter a')).count()).toEqual(1); + expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()) + .toBe('new http://link.'); + }); + + it('should work with the target property', function() { + expect(element(by.id('linky-target')). + element(by.binding("snippetWithTarget | linky:'_blank'")).getText()). + toBe('http://angularjs.org/'); + expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank'); + }); + + + */ +angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { + var LINKY_URL_REGEXP = + /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"”’]/, + MAILTO_REGEXP = /^mailto:/; + + return function(text, target) { + if (!text) return text; + var match; + var raw = text; + var html = []; + var url; + var i; + while ((match = raw.match(LINKY_URL_REGEXP))) { + // We can not end in these as they are sometimes found at the end of the sentence + url = match[0]; + // if we did not match ftp/http/www/mailto then assume mailto + if (!match[2] && !match[4]) { + url = (match[3] ? 'http://' : 'mailto:') + url; + } + i = match.index; + addText(raw.substr(0, i)); + addLink(url, match[0].replace(MAILTO_REGEXP, '')); + raw = raw.substring(i + match[0].length); + } + addText(raw); + return $sanitize(html.join('')); + + function addText(text) { + if (!text) { + return; + } + html.push(sanitizeText(text)); + } + + function addLink(url, text) { + html.push(''); + addText(text); + html.push(''); + } + }; +}]); + + +})(window, window.angular); diff --git a/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js b/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js new file mode 100644 index 0000000000..f57a22b7f8 --- /dev/null +++ b/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js @@ -0,0 +1,19 @@ +describe('Security concerns', function() { + var highlightFilter, $sanitize; + + beforeEach(module('ui.bootstrap.typeahead', 'ngSanitize')); + + beforeEach(inject(function (typeaheadHighlightFilter, _$sanitize_) { + highlightFilter = typeaheadHighlightFilter; + $sanitize = _$sanitize_; + })); + + it('should escape the "script" tag when present', function() { + expect(highlightFilter('before match after', 'match')).toEqual('before match after'); + }); + + it('should escape attributes that execute javascript from html elements', function() { + expect(highlightFilter('before
match after', 'match')).toEqual('before
match after'); + }); + +}); \ No newline at end of file diff --git a/src/typeahead/test/typeahead-highlight.spec.js b/src/typeahead/test/typeahead-highlight.spec.js index 1a5aafc48d..1758b557f1 100644 --- a/src/typeahead/test/typeahead-highlight.spec.js +++ b/src/typeahead/test/typeahead-highlight.spec.js @@ -1,39 +1,50 @@ describe('typeaheadHighlight', function () { - var highlightFilter; + var highlightFilter, $log, logSpy; beforeEach(module('ui.bootstrap.typeahead')); + + beforeEach(inject(function(_$log_) { + $log = _$log_; + logSpy = spyOn($log, 'warn'); + })); + beforeEach(inject(function (typeaheadHighlightFilter) { highlightFilter = typeaheadHighlightFilter; })); it('should higlight a match', function () { - expect(highlightFilter('before match after', 'match')).toEqual('before match after'); + expect(highlightFilter('before match after', 'match').$$unwrapTrustedValue()).toEqual('before match after'); }); it('should higlight a match with mixed case', function () { - expect(highlightFilter('before MaTch after', 'match')).toEqual('before MaTch after'); + expect(highlightFilter('before MaTch after', 'match').$$unwrapTrustedValue()).toEqual('before MaTch after'); }); it('should higlight all matches', function () { - expect(highlightFilter('before MaTch after match', 'match')).toEqual('before MaTch after match'); + expect(highlightFilter('before MaTch after match', 'match').$$unwrapTrustedValue()).toEqual('before MaTch after match'); }); it('should do nothing if no match', function () { - expect(highlightFilter('before match after', 'nomatch')).toEqual('before match after'); + expect(highlightFilter('before match after', 'nomatch').$$unwrapTrustedValue()).toEqual('before match after'); }); it('should do nothing if no or empty query', function () { - expect(highlightFilter('before match after', '')).toEqual('before match after'); - expect(highlightFilter('before match after', null)).toEqual('before match after'); - expect(highlightFilter('before match after', undefined)).toEqual('before match after'); + expect(highlightFilter('before match after', '').$$unwrapTrustedValue()).toEqual('before match after'); + expect(highlightFilter('before match after', null).$$unwrapTrustedValue()).toEqual('before match after'); + expect(highlightFilter('before match after', undefined).$$unwrapTrustedValue()).toEqual('before match after'); }); it('issue 316 - should work correctly for regexp reserved words', function () { - expect(highlightFilter('before (match after', '(match')).toEqual('before (match after'); + expect(highlightFilter('before (match after', '(match').$$unwrapTrustedValue()).toEqual('before (match after'); }); it('issue 1777 - should work correctly with numeric values', function () { - expect(highlightFilter(123, '2')).toEqual('123'); + expect(highlightFilter(123, '2').$$unwrapTrustedValue()).toEqual('123'); }); + + it('should show a warning when this component is being used unsafely', function() { + expect(logSpy).toHaveBeenCalled(); + }); + }); diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 10b7778cf7..4ba885b2fc 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -404,13 +404,30 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap }; }]) - .filter('typeaheadHighlight', function() { + .filter('typeaheadHighlight', ['$sce', '$injector', '$log', function($sce, $injector, $log) { + + var $sanitize; + try { + $sanitize = $injector.get('$sanitize'); //tries to inject the sanitize service + } + catch(e) { + $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger + } function escapeRegexp(queryToEscape) { - return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); + return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); // Regex: capture the whole query string and replace it with the string that will be used to match the results, for example if the capture is "a" the result will be \a } return function(matchItem, query) { - return query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; + if($sanitize) { // Is the $sanitize service available? + if(/<.*>/g.test(matchItem)){ + matchItem = $sanitize(matchItem); // If it's present the string gets sanitized and packed in a $sce object. + } + } + matchItem = query? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag + if(!$sanitize) { + matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive + } + return matchItem; }; - }); + }]); From 06ac2c3df130d5dce9e7df53b333f598759eaadc Mon Sep 17 00:00:00 2001 From: Fernando Ruiz Date: Thu, 2 Apr 2015 18:52:16 -0700 Subject: [PATCH 09/14] fix(typeahead): Refactor of previous fix --- src/typeahead/typeahead.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 4ba885b2fc..2d36b9a5ca 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -415,17 +415,16 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap } function escapeRegexp(queryToEscape) { - return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); // Regex: capture the whole query string and replace it with the string that will be used to match the results, for example if the capture is "a" the result will be \a + // Regex: capture the whole query string and replace it with the string that will be used to match + // the results, for example if the capture is "a" the result will be \a + return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); } return function(matchItem, query) { - if($sanitize) { // Is the $sanitize service available? - if(/<.*>/g.test(matchItem)){ - matchItem = $sanitize(matchItem); // If it's present the string gets sanitized and packed in a $sce object. - } - } matchItem = query? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag - if(!$sanitize) { + if($sanitize) { // Is the $sanitize service available? + matchItem = $sanitize(matchItem); // If it's present the string gets sanitized and packed in a $sce object. + } else { matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive } return matchItem; From c9affb6ff280a7a8b9db11ea6b244cf0efd9a2e1 Mon Sep 17 00:00:00 2001 From: Fernando Ruiz Date: Thu, 2 Apr 2015 18:53:22 -0700 Subject: [PATCH 10/14] fix(typeahead): Refactor of previous fix --- .../test/typeahead-highlight.spec.js | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/typeahead/test/typeahead-highlight.spec.js b/src/typeahead/test/typeahead-highlight.spec.js index 1758b557f1..91ae965f4a 100644 --- a/src/typeahead/test/typeahead-highlight.spec.js +++ b/src/typeahead/test/typeahead-highlight.spec.js @@ -1,11 +1,12 @@ describe('typeaheadHighlight', function () { - var highlightFilter, $log, logSpy; + var highlightFilter, $log, $sce, logSpy; beforeEach(module('ui.bootstrap.typeahead')); - beforeEach(inject(function(_$log_) { + beforeEach(inject(function(_$log_, _$sce_) { $log = _$log_; + $sce = _$sce_; logSpy = spyOn($log, 'warn'); })); @@ -14,33 +15,33 @@ describe('typeaheadHighlight', function () { })); it('should higlight a match', function () { - expect(highlightFilter('before match after', 'match').$$unwrapTrustedValue()).toEqual('before match after'); + expect($sce.getTrustedHtml(highlightFilter('before match after', 'match'))).toEqual('before match after'); }); it('should higlight a match with mixed case', function () { - expect(highlightFilter('before MaTch after', 'match').$$unwrapTrustedValue()).toEqual('before MaTch after'); + expect($sce.getTrustedHtml(highlightFilter('before MaTch after', 'match'))).toEqual('before MaTch after'); }); it('should higlight all matches', function () { - expect(highlightFilter('before MaTch after match', 'match').$$unwrapTrustedValue()).toEqual('before MaTch after match'); + expect($sce.getTrustedHtml(highlightFilter('before MaTch after match', 'match'))).toEqual('before MaTch after match'); }); it('should do nothing if no match', function () { - expect(highlightFilter('before match after', 'nomatch').$$unwrapTrustedValue()).toEqual('before match after'); + expect($sce.getTrustedHtml(highlightFilter('before match after', 'nomatch'))).toEqual('before match after'); }); it('should do nothing if no or empty query', function () { - expect(highlightFilter('before match after', '').$$unwrapTrustedValue()).toEqual('before match after'); - expect(highlightFilter('before match after', null).$$unwrapTrustedValue()).toEqual('before match after'); - expect(highlightFilter('before match after', undefined).$$unwrapTrustedValue()).toEqual('before match after'); + expect($sce.getTrustedHtml(highlightFilter('before match after', ''))).toEqual('before match after'); + expect($sce.getTrustedHtml(highlightFilter('before match after', null))).toEqual('before match after'); + expect($sce.getTrustedHtml(highlightFilter('before match after', undefined))).toEqual('before match after'); }); it('issue 316 - should work correctly for regexp reserved words', function () { - expect(highlightFilter('before (match after', '(match').$$unwrapTrustedValue()).toEqual('before (match after'); + expect($sce.getTrustedHtml(highlightFilter('before (match after', '(match'))).toEqual('before (match after'); }); it('issue 1777 - should work correctly with numeric values', function () { - expect(highlightFilter(123, '2').$$unwrapTrustedValue()).toEqual('123'); + expect($sce.getTrustedHtml(highlightFilter(123, '2'))).toEqual('123'); }); it('should show a warning when this component is being used unsafely', function() { From d3b707b3ec72a3c0f550e2e7a9ed66d4aae44e6e Mon Sep 17 00:00:00 2001 From: Fernando Ruiz Date: Thu, 9 Apr 2015 20:18:17 -0700 Subject: [PATCH 11/14] fix(typeahead): Changes to typeaheadHighlight --- misc/demo/assets/app.js | 2 +- .../test/typeahead-highlight-ngsanitize.spec.js | 16 ++++++++-------- src/typeahead/test/typeahead-highlight.spec.js | 1 + src/typeahead/typeahead.js | 13 +++++++++---- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/misc/demo/assets/app.js b/misc/demo/assets/app.js index af0c4a8ff1..8d19f5481b 100644 --- a/misc/demo/assets/app.js +++ b/misc/demo/assets/app.js @@ -1,5 +1,5 @@ /* global FastClick, smoothScroll */ -angular.module('ui.bootstrap.demo', ['ui.bootstrap', 'plunker', 'ngTouch', 'ngAnimate'], function($httpProvider){ +angular.module('ui.bootstrap.demo', ['ui.bootstrap', 'plunker', 'ngTouch', 'ngAnimate', 'ngSanitize'], function($httpProvider){ FastClick.attach(document.body); delete $httpProvider.defaults.headers.common['X-Requested-With']; }).run(['$location', function($location){ diff --git a/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js b/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js index f57a22b7f8..b659baad2a 100644 --- a/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js +++ b/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js @@ -1,19 +1,19 @@ describe('Security concerns', function() { - var highlightFilter, $sanitize; + var highlightFilter, + $sanitize, + logSpy; beforeEach(module('ui.bootstrap.typeahead', 'ngSanitize')); - beforeEach(inject(function (typeaheadHighlightFilter, _$sanitize_) { + beforeEach(inject(function (typeaheadHighlightFilter, _$sanitize_, $log) { highlightFilter = typeaheadHighlightFilter; $sanitize = _$sanitize_; + logSpy = spyOn($log, 'warn'); })); - it('should escape the "script" tag when present', function() { - expect(highlightFilter('before match after', 'match')).toEqual('before match after'); - }); - - it('should escape attributes that execute javascript from html elements', function() { - expect(highlightFilter('before
match after', 'match')).toEqual('before
match after'); + it('should not call the $log service when ngSanitize is present', function() { + highlightFilter('before after', 'match'); + expect(logSpy).not.toHaveBeenCalled(); }); }); \ No newline at end of file diff --git a/src/typeahead/test/typeahead-highlight.spec.js b/src/typeahead/test/typeahead-highlight.spec.js index 91ae965f4a..9ed3020904 100644 --- a/src/typeahead/test/typeahead-highlight.spec.js +++ b/src/typeahead/test/typeahead-highlight.spec.js @@ -45,6 +45,7 @@ describe('typeaheadHighlight', function () { }); it('should show a warning when this component is being used unsafely', function() { + highlightFilter('before match after', 'match'); expect(logSpy).toHaveBeenCalled(); }); diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 2d36b9a5ca..6648a397bb 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -411,7 +411,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap $sanitize = $injector.get('$sanitize'); //tries to inject the sanitize service } catch(e) { - $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger + // Catches the exception when the $sanitize service is not available. } function escapeRegexp(queryToEscape) { @@ -420,11 +420,16 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); } + function containsHtml(matchItem) { + return /<.*>/g.test(matchItem); + } + return function(matchItem, query) { + if(!$sanitize && containsHtml(matchItem)) { + $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger + } matchItem = query? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag - if($sanitize) { // Is the $sanitize service available? - matchItem = $sanitize(matchItem); // If it's present the string gets sanitized and packed in a $sce object. - } else { + if(!$sanitize) { matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive } return matchItem; From 51e5c6cc1a08fa572ba08c2808fa7c233f37bcda Mon Sep 17 00:00:00 2001 From: Fernando Ruiz Date: Thu, 9 Apr 2015 20:23:50 -0700 Subject: [PATCH 12/14] fix(typeahead): Removed dependency ui.bootstrap.bindHtml --- src/typeahead/typeahead.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 6648a397bb..880f0098ea 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -1,4 +1,4 @@ -angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml']) +angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) /** * A helper service that can parse typeahead's syntax (string provided by users) From a26d38a5f777899d3d08ef56d772e40bba6a0d89 Mon Sep 17 00:00:00 2001 From: Fernando Ruiz Date: Mon, 13 Apr 2015 19:04:25 -0700 Subject: [PATCH 13/14] fix(typeahead): Refactored code --- misc/test-lib/angular-sanitize.js | 681 ------------------------------ src/typeahead/typeahead.js | 13 +- 2 files changed, 4 insertions(+), 690 deletions(-) delete mode 100644 misc/test-lib/angular-sanitize.js diff --git a/misc/test-lib/angular-sanitize.js b/misc/test-lib/angular-sanitize.js deleted file mode 100644 index b629a52433..0000000000 --- a/misc/test-lib/angular-sanitize.js +++ /dev/null @@ -1,681 +0,0 @@ -/** - * @license AngularJS v1.3.13 - * (c) 2010-2014 Google, Inc. http://angularjs.org - * License: MIT - */ -(function(window, angular, undefined) {'use strict'; - -var $sanitizeMinErr = angular.$$minErr('$sanitize'); - -/** - * @ngdoc module - * @name ngSanitize - * @description - * - * # ngSanitize - * - * The `ngSanitize` module provides functionality to sanitize HTML. - * - * - *
- * - * See {@link ngSanitize.$sanitize `$sanitize`} for usage. - */ - -/* - * HTML Parser By Misko Hevery (misko@hevery.com) - * based on: HTML Parser By John Resig (ejohn.org) - * Original code by Erik Arvidsson, Mozilla Public License - * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js - * - * // Use like so: - * htmlParser(htmlString, { - * start: function(tag, attrs, unary) {}, - * end: function(tag) {}, - * chars: function(text) {}, - * comment: function(text) {} - * }); - * - */ - - -/** - * @ngdoc service - * @name $sanitize - * @kind function - * - * @description - * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are - * then serialized back to properly escaped html string. This means that no unsafe input can make - * it into the returned string, however, since our parser is more strict than a typical browser - * parser, it's possible that some obscure input, which would be recognized as valid HTML by a - * browser, won't make it through the sanitizer. The input may also contain SVG markup. - * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and - * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}. - * - * @param {string} html HTML input. - * @returns {string} Sanitized HTML. - * - * @example - - - -
- Snippet: - - - - - - - - - - - - - - - - - - - - - - - - - -
DirectiveHowSourceRendered
ng-bind-htmlAutomatically uses $sanitize
<div ng-bind-html="snippet">
</div>
ng-bind-htmlBypass $sanitize by explicitly trusting the dangerous value -
<div ng-bind-html="deliberatelyTrustDangerousSnippet()">
-</div>
-
ng-bindAutomatically escapes
<div ng-bind="snippet">
</div>
-
-
- - it('should sanitize the html snippet by default', function() { - expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). - toBe('

an html\nclick here\nsnippet

'); - }); - - it('should inline raw snippet if bound to a trusted value', function() { - expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()). - toBe("

an html\n" + - "click here\n" + - "snippet

"); - }); - - it('should escape snippet without any filter', function() { - expect(element(by.css('#bind-default div')).getInnerHtml()). - toBe("<p style=\"color:blue\">an html\n" + - "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + - "snippet</p>"); - }); - - it('should update', function() { - element(by.model('snippet')).clear(); - element(by.model('snippet')).sendKeys('new text'); - expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). - toBe('new text'); - expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe( - 'new text'); - expect(element(by.css('#bind-default div')).getInnerHtml()).toBe( - "new <b onclick=\"alert(1)\">text</b>"); - }); -
-
- */ -function $SanitizeProvider() { - this.$get = ['$$sanitizeUri', function($$sanitizeUri) { - return function(html) { - var buf = []; - htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) { - return !/^unsafe/.test($$sanitizeUri(uri, isImage)); - })); - return buf.join(''); - }; - }]; -} - -function sanitizeText(chars) { - var buf = []; - var writer = htmlSanitizeWriter(buf, angular.noop); - writer.chars(chars); - return buf.join(''); -} - - -// Regular Expressions for parsing tags and attributes -var START_TAG_REGEXP = - /^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/, - END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/, - ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, - BEGIN_TAG_REGEXP = /^/g, - DOCTYPE_REGEXP = /]*?)>/i, - CDATA_REGEXP = //g, - SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, - // Match everything outside of normal chars and " (quote character) - NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; - - -// Good source of info about elements and attributes -// http://dev.w3.org/html5/spec/Overview.html#semantics -// http://simon.html5.org/html-elements - -// Safe Void Elements - HTML5 -// http://dev.w3.org/html5/spec/Overview.html#void-elements -var voidElements = makeMap("area,br,col,hr,img,wbr"); - -// Elements that you can, intentionally, leave open (and which close themselves) -// http://dev.w3.org/html5/spec/Overview.html#optional-tags -var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), - optionalEndTagInlineElements = makeMap("rp,rt"), - optionalEndTagElements = angular.extend({}, - optionalEndTagInlineElements, - optionalEndTagBlockElements); - -// Safe Block Elements - HTML5 -var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," + - "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," + - "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); - -// Inline Elements - HTML5 -var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," + - "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," + - "samp,small,span,strike,strong,sub,sup,time,tt,u,var")); - -// SVG Elements -// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements -var svgElements = makeMap("animate,animateColor,animateMotion,animateTransform,circle,defs," + - "desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,hkern,image,linearGradient," + - "line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,radialGradient,rect,set," + - "stop,svg,switch,text,title,tspan,use"); - -// Special Elements (can contain anything) -var specialElements = makeMap("script,style"); - -var validElements = angular.extend({}, - voidElements, - blockElements, - inlineElements, - optionalEndTagElements, - svgElements); - -//Attributes that have href and hence need to be sanitized -var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap,xlink:href"); - -var htmlAttrs = makeMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' + - 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' + - 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' + - 'scope,scrolling,shape,size,span,start,summary,target,title,type,' + - 'valign,value,vspace,width'); - -// SVG attributes (without "id" and "name" attributes) -// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes -var svgAttrs = makeMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' + - 'attributeName,attributeType,baseProfile,bbox,begin,by,calcMode,cap-height,class,color,' + - 'color-rendering,content,cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,' + - 'font-size,font-stretch,font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,' + - 'gradientUnits,hanging,height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,' + - 'keySplines,keyTimes,lang,marker-end,marker-mid,marker-start,markerHeight,markerUnits,' + - 'markerWidth,mathematical,max,min,offset,opacity,orient,origin,overline-position,' + - 'overline-thickness,panose-1,path,pathLength,points,preserveAspectRatio,r,refX,refY,' + - 'repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,' + - 'stemv,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke,' + - 'stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,' + - 'stroke-opacity,stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,' + - 'underline-position,underline-thickness,unicode,unicode-range,units-per-em,values,version,' + - 'viewBox,visibility,width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,' + - 'xlink:show,xlink:title,xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,' + - 'zoomAndPan'); - -var validAttrs = angular.extend({}, - uriAttrs, - svgAttrs, - htmlAttrs); - -function makeMap(str) { - var obj = {}, items = str.split(','), i; - for (i = 0; i < items.length; i++) obj[items[i]] = true; - return obj; -} - - -/** - * @example - * htmlParser(htmlString, { - * start: function(tag, attrs, unary) {}, - * end: function(tag) {}, - * chars: function(text) {}, - * comment: function(text) {} - * }); - * - * @param {string} html string - * @param {object} handler - */ -function htmlParser(html, handler) { - if (typeof html !== 'string') { - if (html === null || typeof html === 'undefined') { - html = ''; - } else { - html = '' + html; - } - } - var index, chars, match, stack = [], last = html, text; - stack.last = function() { return stack[stack.length - 1]; }; - - while (html) { - text = ''; - chars = true; - - // Make sure we're not in a script or style element - if (!stack.last() || !specialElements[stack.last()]) { - - // Comment - if (html.indexOf("", index) === index) { - if (handler.comment) handler.comment(html.substring(4, index)); - html = html.substring(index + 3); - chars = false; - } - // DOCTYPE - } else if (DOCTYPE_REGEXP.test(html)) { - match = html.match(DOCTYPE_REGEXP); - - if (match) { - html = html.replace(match[0], ''); - chars = false; - } - // end tag - } else if (BEGING_END_TAGE_REGEXP.test(html)) { - match = html.match(END_TAG_REGEXP); - - if (match) { - html = html.substring(match[0].length); - match[0].replace(END_TAG_REGEXP, parseEndTag); - chars = false; - } - - // start tag - } else if (BEGIN_TAG_REGEXP.test(html)) { - match = html.match(START_TAG_REGEXP); - - if (match) { - // We only have a valid start-tag if there is a '>'. - if (match[4]) { - html = html.substring(match[0].length); - match[0].replace(START_TAG_REGEXP, parseStartTag); - } - chars = false; - } else { - // no ending tag found --- this piece should be encoded as an entity. - text += '<'; - html = html.substring(1); - } - } - - if (chars) { - index = html.indexOf("<"); - - text += index < 0 ? html : html.substring(0, index); - html = index < 0 ? "" : html.substring(index); - - if (handler.chars) handler.chars(decodeEntities(text)); - } - - } else { - // IE versions 9 and 10 do not understand the regex '[^]', so using a workaround with [\W\w]. - html = html.replace(new RegExp("([\\W\\w]*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), - function(all, text) { - text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1"); - - if (handler.chars) handler.chars(decodeEntities(text)); - - return ""; - }); - - parseEndTag("", stack.last()); - } - - if (html == last) { - throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " + - "of html: {0}", html); - } - last = html; - } - - // Clean up any remaining tags - parseEndTag(); - - function parseStartTag(tag, tagName, rest, unary) { - tagName = angular.lowercase(tagName); - if (blockElements[tagName]) { - while (stack.last() && inlineElements[stack.last()]) { - parseEndTag("", stack.last()); - } - } - - if (optionalEndTagElements[tagName] && stack.last() == tagName) { - parseEndTag("", tagName); - } - - unary = voidElements[tagName] || !!unary; - - if (!unary) - stack.push(tagName); - - var attrs = {}; - - rest.replace(ATTR_REGEXP, - function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) { - var value = doubleQuotedValue - || singleQuotedValue - || unquotedValue - || ''; - - attrs[name] = decodeEntities(value); - }); - if (handler.start) handler.start(tagName, attrs, unary); - } - - function parseEndTag(tag, tagName) { - var pos = 0, i; - tagName = angular.lowercase(tagName); - if (tagName) - // Find the closest opened tag of the same type - for (pos = stack.length - 1; pos >= 0; pos--) - if (stack[pos] == tagName) - break; - - if (pos >= 0) { - // Close all the open elements, up the stack - for (i = stack.length - 1; i >= pos; i--) - if (handler.end) handler.end(stack[i]); - - // Remove the open elements from the stack - stack.length = pos; - } - } -} - -var hiddenPre=document.createElement("pre"); -var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/; -/** - * decodes all entities into regular string - * @param value - * @returns {string} A string with decoded entities. - */ -function decodeEntities(value) { - if (!value) { return ''; } - - // Note: IE8 does not preserve spaces at the start/end of innerHTML - // so we must capture them and reattach them afterward - var parts = spaceRe.exec(value); - var spaceBefore = parts[1]; - var spaceAfter = parts[3]; - var content = parts[2]; - if (content) { - hiddenPre.innerHTML=content.replace(//g, '>'); -} - -/** - * create an HTML/XML writer which writes to buffer - * @param {Array} buf use buf.jain('') to get out sanitized html string - * @returns {object} in the form of { - * start: function(tag, attrs, unary) {}, - * end: function(tag) {}, - * chars: function(text) {}, - * comment: function(text) {} - * } - */ -function htmlSanitizeWriter(buf, uriValidator) { - var ignore = false; - var out = angular.bind(buf, buf.push); - return { - start: function(tag, attrs, unary) { - tag = angular.lowercase(tag); - if (!ignore && specialElements[tag]) { - ignore = tag; - } - if (!ignore && validElements[tag] === true) { - out('<'); - out(tag); - angular.forEach(attrs, function(value, key) { - var lkey=angular.lowercase(key); - var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background'); - if (validAttrs[lkey] === true && - (uriAttrs[lkey] !== true || uriValidator(value, isImage))) { - out(' '); - out(key); - out('="'); - out(encodeEntities(value)); - out('"'); - } - }); - out(unary ? '/>' : '>'); - } - }, - end: function(tag) { - tag = angular.lowercase(tag); - if (!ignore && validElements[tag] === true) { - out(''); - } - if (tag == ignore) { - ignore = false; - } - }, - chars: function(chars) { - if (!ignore) { - out(encodeEntities(chars)); - } - } - }; -} - - -// define ngSanitize module and register $sanitize service -angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); - -/* global sanitizeText: false */ - -/** - * @ngdoc filter - * @name linky - * @kind function - * - * @description - * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and - * plain email address links. - * - * Requires the {@link ngSanitize `ngSanitize`} module to be installed. - * - * @param {string} text Input text. - * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in. - * @returns {string} Html-linkified text. - * - * @usage - - * - * @example - - - -
- Snippet: - - - - - - - - - - - - - - - - - - - - - -
FilterSourceRendered
linky filter -
<div ng-bind-html="snippet | linky">
</div>
-
-
-
linky target -
<div ng-bind-html="snippetWithTarget | linky:'_blank'">
</div>
-
-
-
no filter
<div ng-bind="snippet">
</div>
- - - it('should linkify the snippet with urls', function() { - expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). - toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' + - 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); - expect(element.all(by.css('#linky-filter a')).count()).toEqual(4); - }); - - it('should not linkify snippet without the linky filter', function() { - expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()). - toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' + - 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); - expect(element.all(by.css('#escaped-html a')).count()).toEqual(0); - }); - - it('should update', function() { - element(by.model('snippet')).clear(); - element(by.model('snippet')).sendKeys('new http://link.'); - expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). - toBe('new http://link.'); - expect(element.all(by.css('#linky-filter a')).count()).toEqual(1); - expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()) - .toBe('new http://link.'); - }); - - it('should work with the target property', function() { - expect(element(by.id('linky-target')). - element(by.binding("snippetWithTarget | linky:'_blank'")).getText()). - toBe('http://angularjs.org/'); - expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank'); - }); - - - */ -angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { - var LINKY_URL_REGEXP = - /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"”’]/, - MAILTO_REGEXP = /^mailto:/; - - return function(text, target) { - if (!text) return text; - var match; - var raw = text; - var html = []; - var url; - var i; - while ((match = raw.match(LINKY_URL_REGEXP))) { - // We can not end in these as they are sometimes found at the end of the sentence - url = match[0]; - // if we did not match ftp/http/www/mailto then assume mailto - if (!match[2] && !match[4]) { - url = (match[3] ? 'http://' : 'mailto:') + url; - } - i = match.index; - addText(raw.substr(0, i)); - addLink(url, match[0].replace(MAILTO_REGEXP, '')); - raw = raw.substring(i + match[0].length); - } - addText(raw); - return $sanitize(html.join('')); - - function addText(text) { - if (!text) { - return; - } - html.push(sanitizeText(text)); - } - - function addLink(url, text) { - html.push(''); - addText(text); - html.push(''); - } - }; -}]); - - -})(window, window.angular); diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 880f0098ea..a08d4b6f2c 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -406,13 +406,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) .filter('typeaheadHighlight', ['$sce', '$injector', '$log', function($sce, $injector, $log) { - var $sanitize; - try { - $sanitize = $injector.get('$sanitize'); //tries to inject the sanitize service - } - catch(e) { - // Catches the exception when the $sanitize service is not available. - } + var isSanitizePresent; + isSanitizePresent = $injector.has('$sanitize'); function escapeRegexp(queryToEscape) { // Regex: capture the whole query string and replace it with the string that will be used to match @@ -425,11 +420,11 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) } return function(matchItem, query) { - if(!$sanitize && containsHtml(matchItem)) { + if(!isSanitizePresent && containsHtml(matchItem)) { $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger } matchItem = query? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag - if(!$sanitize) { + if(!isSanitizePresent) { matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive } return matchItem; From 7572cab8f851250e1ab84d17e0f4edd1b4a588f7 Mon Sep 17 00:00:00 2001 From: Fernando Ruiz Date: Tue, 14 Apr 2015 19:12:49 -0700 Subject: [PATCH 14/14] fix(typeahead): Code groom. --- src/typeahead/test/typeahead-highlight-ngsanitize.spec.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js b/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js index b659baad2a..17c445528a 100644 --- a/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js +++ b/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js @@ -1,7 +1,5 @@ describe('Security concerns', function() { - var highlightFilter, - $sanitize, - logSpy; + var highlightFilter, $sanitize, logSpy; beforeEach(module('ui.bootstrap.typeahead', 'ngSanitize'));