This repository has been archived by the owner on Dec 30, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 157
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(Highlight, Snippet): prevent XSS via routing (#783)
Co-authored-by: Haroen Viaene <hello@haroen.me>
- Loading branch information
Showing
9 changed files
with
465 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
<template> | ||
<span | ||
:class="suit()" | ||
> | ||
<component | ||
v-for="({ value, isHighlighted }, index) in parsedHighlights" | ||
:class="[isHighlighted && suit('highlighted')]" | ||
:key="index" | ||
:is="isHighlighted ? highlightedTagName : textNode" | ||
>{{ value }}</component> | ||
</span> | ||
</template> | ||
|
||
<script> | ||
import { parseAlgoliaHit } from '../util/parseAlgoliaHit'; | ||
export default { | ||
name: 'AisHighlighter', | ||
props: { | ||
hit: { | ||
type: Object, | ||
required: true, | ||
}, | ||
attribute: { | ||
type: String, | ||
required: true, | ||
}, | ||
highlightedTagName: { | ||
type: String, | ||
default: 'mark', | ||
}, | ||
suit: { type: Function, required: true }, | ||
highlightProperty: { type: String, required: true }, | ||
preTag: { type: String, required: true }, | ||
postTag: { type: String, required: true }, | ||
}, | ||
data() { | ||
return { | ||
textNode: { | ||
functional: true, | ||
render(createElement, context) { | ||
const slots = context.slots(); | ||
return slots.default; | ||
}, | ||
}, | ||
}; | ||
}, | ||
computed: { | ||
parsedHighlights() { | ||
return parseAlgoliaHit({ | ||
attribute: this.attribute, | ||
hit: this.hit, | ||
highlightProperty: this.highlightProperty, | ||
preTag: this.preTag, | ||
postTag: this.postTag, | ||
}); | ||
}, | ||
}, | ||
}; | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
// copied from React InstantSearch | ||
import { parseAlgoliaHit } from '../parseAlgoliaHit'; | ||
|
||
describe('parseAlgoliaHit()', () => { | ||
it('it does not break when there is a missing attribute', () => { | ||
const attribute = 'attr'; | ||
const out = parseAlgoliaHit({ | ||
attribute, | ||
hit: {}, | ||
highlightProperty: '_highlightResult', | ||
}); | ||
expect(out).toEqual([]); | ||
}); | ||
|
||
it('creates a single element when there is no tag', () => { | ||
const value = 'foo bar baz'; | ||
const attribute = 'attr'; | ||
const out = parseAlgoliaHit({ | ||
attribute, | ||
hit: createHit(attribute, value), | ||
highlightProperty: '_highlightResult', | ||
}); | ||
expect(out).toEqual([{ isHighlighted: false, value }]); | ||
}); | ||
|
||
it('creates a single element when there is only a tag', () => { | ||
const textValue = 'foo bar baz'; | ||
const value = `__ais-highlight__${textValue}__/ais-highlight__`; | ||
const attribute = 'attr'; | ||
const out = parseAlgoliaHit({ | ||
attribute, | ||
hit: createHit(attribute, value), | ||
highlightProperty: '_highlightResult', | ||
}); | ||
expect(out).toEqual([{ value: textValue, isHighlighted: true }]); | ||
}); | ||
|
||
it('fetches and parses a deep attribute', () => { | ||
const textValue = 'foo bar baz'; | ||
const value = `__ais-highlight__${textValue}__/ais-highlight__`; | ||
const hit = { | ||
lvl0: { lvl1: { lvl2: value } }, | ||
_highlightResult: { | ||
lvl0: { lvl1: { lvl2: { value } } }, | ||
}, | ||
}; | ||
const out = parseAlgoliaHit({ | ||
attribute: 'lvl0.lvl1.lvl2', | ||
hit, | ||
highlightProperty: '_highlightResult', | ||
}); | ||
expect(out).toEqual([{ value: textValue, isHighlighted: true }]); | ||
}); | ||
|
||
it('parses the string and returns the part that are highlighted - 1 big highlight', () => { | ||
const str = | ||
'like __ais-highlight__al__/ais-highlight__golia does __ais-highlight__al__/ais-highlight__golia'; | ||
const hit = createHit('attr', str); | ||
const parsed = parseAlgoliaHit({ | ||
attribute: 'attr', | ||
hit, | ||
highlightProperty: '_highlightResult', | ||
}); | ||
expect(parsed).toEqual([ | ||
{ value: 'like ', isHighlighted: false }, | ||
{ value: 'al', isHighlighted: true }, | ||
{ value: 'golia does ', isHighlighted: false }, | ||
{ value: 'al', isHighlighted: true }, | ||
{ value: 'golia', isHighlighted: false }, | ||
]); | ||
}); | ||
|
||
it('supports the array format, parses it and returns the part that is highlighted', () => { | ||
const hit = { | ||
tags: ['litterature', 'biology', 'photography'], | ||
_highlightResult: { | ||
tags: [ | ||
{ value: 'litterature' }, | ||
{ value: 'biology' }, | ||
{ value: '__ais-highlight__photo__/ais-highlight__graphy' }, | ||
], | ||
}, | ||
}; | ||
|
||
const actual = parseAlgoliaHit({ | ||
attribute: 'tags', | ||
hit, | ||
highlightProperty: '_highlightResult', | ||
}); | ||
|
||
const exepectation = [ | ||
[{ value: 'litterature', isHighlighted: false }], | ||
[{ value: 'biology', isHighlighted: false }], | ||
[ | ||
{ value: 'photo', isHighlighted: true }, | ||
{ value: 'graphy', isHighlighted: false }, | ||
], | ||
]; | ||
|
||
expect(actual).toEqual(exepectation); | ||
}); | ||
|
||
it('parses the string and returns the part that are highlighted - same pre and post tag', () => { | ||
const str = 'surpise **lo**l mouhahah roflmao **lo**utre'; | ||
const hit = createHit('attr', str); | ||
const parsed = parseAlgoliaHit({ | ||
preTag: '**', | ||
postTag: '**', | ||
attribute: 'attr', | ||
hit, | ||
highlightProperty: '_highlightResult', | ||
}); | ||
expect(parsed).toEqual([ | ||
{ value: 'surpise ', isHighlighted: false }, | ||
{ value: 'lo', isHighlighted: true }, | ||
{ value: 'l mouhahah roflmao ', isHighlighted: false }, | ||
{ value: 'lo', isHighlighted: true }, | ||
{ value: 'utre', isHighlighted: false }, | ||
]); | ||
}); | ||
|
||
it('throws when hit is `null`', () => { | ||
expect( | ||
parseAlgoliaHit.bind(null, { | ||
attribute: 'unknownattribute', | ||
hit: null, | ||
highlightProperty: '_highlightResult', | ||
}) | ||
).toThrow('`hit`, the matching record, must be provided'); | ||
}); | ||
|
||
it('throws when hit is `undefined`', () => { | ||
expect( | ||
parseAlgoliaHit.bind(null, { | ||
attribute: 'unknownAttribute', | ||
hit: undefined, | ||
highlightProperty: '_highlightResult', | ||
}) | ||
).toThrow('`hit`, the matching record, must be provided'); | ||
}); | ||
|
||
it('unescapes escaped strings', () => { | ||
expect( | ||
parseAlgoliaHit({ | ||
attribute: 'title', | ||
hit: { | ||
_highlightResult: { | ||
title: { | ||
value: ''TV' & <Home> "Theater"', | ||
}, | ||
}, | ||
}, | ||
highlightProperty: '_highlightResult', | ||
}) | ||
).toEqual([ | ||
{ | ||
isHighlighted: false, | ||
value: `'TV' & <Home> "Theater"`, | ||
}, | ||
]); | ||
|
||
expect( | ||
parseAlgoliaHit({ | ||
attribute: 'categories', | ||
hit: { | ||
_highlightResult: { | ||
categories: [ | ||
{ | ||
value: 'TV & Home Theater', | ||
}, | ||
{ | ||
value: '__ais-highlight__'TV'__/ais-highlight__', | ||
}, | ||
], | ||
}, | ||
}, | ||
highlightProperty: '_highlightResult', | ||
}) | ||
).toEqual([ | ||
[ | ||
{ | ||
isHighlighted: false, | ||
value: `TV & Home Theater`, | ||
}, | ||
], | ||
[ | ||
{ | ||
isHighlighted: true, | ||
value: `'TV'`, | ||
}, | ||
], | ||
]); | ||
}); | ||
}); | ||
|
||
function createHit(attribute, value) { | ||
return { | ||
[attribute]: value, | ||
_highlightResult: { | ||
[attribute]: { value }, | ||
}, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { unescape } from '../unescape'; | ||
|
||
describe('unescape', () => { | ||
it('unescapes value', () => { | ||
expect(unescape('fred, barney, & pebbles')).toBe( | ||
'fred, barney, & pebbles' | ||
); | ||
|
||
expect(unescape('&<>"'/')).toEqual('&<>"\'/'); | ||
}); | ||
|
||
it('handles strings with nothing to unescape', () => { | ||
expect(unescape('abc')).toEqual('abc'); | ||
}); | ||
|
||
it('does not unescape the "`" character', () => { | ||
expect(unescape('`')).toEqual('`'); | ||
}); | ||
|
||
it('does not unescape the "/" character', () => { | ||
expect(unescape('/')).toEqual('/'); | ||
}); | ||
|
||
it('handles strings with tags', () => { | ||
expect(unescape('<mark>TV & Home Theater</mark>')).toBe( | ||
'<mark>TV & Home Theater</mark>' | ||
); | ||
}); | ||
}); |
Oops, something went wrong.