Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

hrefのXSSを防ぐaタグを作成 #30

Merged
merged 3 commits into from
Apr 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions src/components/ProtectedLink/ProtectedLink.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import Vue from 'vue'
import VueCompositionApi from '@vue/composition-api'
Vue.use(VueCompositionApi);

import { Meta, Props, Story, Preview } from '@storybook/addon-docs/blocks';
import ProtectedLink from './ProtectedLink.vue';

<Meta title="ProtectedLink" component={ProtectedLink} />

# ProtectedLink

<Preview>
<Story name="default">
{{
components: { ProtectedLink },
template: '<ProtectedLink href="//placehold.jp/150x150.png"> //placehold.jp/150x150.png </ProtectedLink>',
}}
</Story>
<Story name="http">
{{
components: { ProtectedLink },
template: '<ProtectedLink href="http://placehold.jp/150x150.png"> http://placehold.jp/150x150.png </ProtectedLink>',
}}
</Story>
<Story name="https">
{{
components: { ProtectedLink },
template: '<ProtectedLink href="https://placehold.jp/150x150.png"> https://placehold.jp/150x150.png </ProtectedLink>',
}}
</Story>
<Story name="absolute path">
{{
components: { ProtectedLink },
template: '<ProtectedLink href="/example"> /example </ProtectedLink>',
}}
</Story>
<Story name="relative path1">
{{
components: { ProtectedLink },
template: '<ProtectedLink href="./example"> ./example </ProtectedLink>',
}}
</Story>
<Story name="relative path2">
{{
components: { ProtectedLink },
template: '<ProtectedLink href="example"> example </ProtectedLink>',
}}
</Story>
<Story name="valid attrs inherited">
{{
components: { ProtectedLink },
template: `
<ProtectedLink
href="https://placehold.jp/150x150.png"
>
https://placehold.jp/150x150.png
</ProtectedLink>
`,
}}
</Story>
<Story name="xss javascript scheme">
{{
components: { ProtectedLink },
template: `
<ProtectedLink
href="javascript:alert(document.domain)"
>
javascript:alert(document.domain)
</ProtectedLink>
`,
}}
</Story>
<Story name="force permit any link">
{{
components: { ProtectedLink },
template: `
<ProtectedLink
href="javascript:alert(document.domain)"
force
>
javascript:alert(document.domain)
</ProtectedLink>
`,
}}
</Story>
</Preview>

```typescript
import { ProtectedLink } from 'lapras-frontend'
```

```html
<ProtectedLink src="//placehold.jp/150x150.png" />
```

<Props of={ProtectedLink} />
50 changes: 50 additions & 0 deletions src/components/ProtectedLink/ProtectedLink.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<template>
<a v-bind="$attrs" :href="_href"><slot></slot></a>
</template>

<script lang="ts">
import { defineComponent } from '@vue/composition-api'

// http://koseki.hatenablog.com/entry/20120212/uricolon
// https://github.com/masatokinugawa/filterbypass/wiki/Browser's-XSS-Filter-Bypass-Cheat-Sheet
// http://nootropic.me/blog/blog/2015/01/30/javascript%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%A0%E3%81%AF%E3%81%82%E3%81%8D%E3%82%89%E3%82%81%E3%81%AA%E3%81%84/
// 上記を方針にhttp:, https以外のスキームの利用の禁止する
// クエリパラメータとして:がvalueにある場合は、forceをつけることで許容する
const filterXSSScheme = (attr: string | undefined): string | undefined => {
if (!attr) return undefined
if (attr.includes(':') && !attr.match(/^https?:\/\//)) {
return undefined
}
return attr
}

type ProtectedLinkProps = {
href: string | undefined
force: boolean
}

export default defineComponent<ProtectedLinkProps>({
// root属性によるcomputedのオーバーライドを防ぐ
// https://jp.vuejs.org/v2/guide/components-props.html
inheritAttrs: false,
props: {
href: {
type: String,
validator(value: string) {
// validator type safe, output console even if force set
return !!filterXSSScheme(value)
},
},
force: {
type: Boolean,
default: false,
},
},
computed: {
_href(this: ProtectedLinkProps): string | undefined {
if (this.force) return this.href
return filterXSSScheme(this.href as string)
},
},
})
</script>
2 changes: 2 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import EnhancedIconButton from '@/components/EnhancedIconButton/EnhancedIconButt
import FieldGroup from '@/components/FieldGroup/FieldGroup.vue'
import FlatButton from '@/components/FlatButton/FlatButton.vue'
import Icon from '@/components/Icon/Icon.vue'
import ProtectedLink from '@/components/ProtectedLink/ProtectedLink.vue'
import Modal from '@/components/Modal/Modal.vue'
import Radio from '@/components/Radio/Radio.vue'
import RatingBar from '@/components/RatingBar/RatingBar.vue'
Expand All @@ -24,6 +25,7 @@ export {
FlatButton,
Icon,
Modal,
ProtectedLink,
Radio,
RatingBar,
SelectBox,
Expand Down