Skip to content

Commit

Permalink
chore(seaside): #185 fix regexp backtracking vulnerability
Browse files Browse the repository at this point in the history
  • Loading branch information
Marthym committed Nov 19, 2023
1 parent 0f11945 commit 461f1b7
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 198 deletions.
311 changes: 155 additions & 156 deletions seaside/src/administration/component/usereditor/UserEditor.vue
Original file line number Diff line number Diff line change
@@ -1,184 +1,183 @@
<template>
<div class="grid bg-base-200 bg-opacity-60 z-30 w-full h-full absolute top-0 left-0 overflow-hidden"
@click="opened = false">
<Transition
enter-active-class="lg:duration-300 ease-in-out"
enter-from-class="lg:transform lg:translate-x-full"
enter-to-class="lg:translate-x-0"
leave-active-class="lg:duration-300 ease-in-out"
leave-from-class="lg:translate-x-0"
leave-to-class="lg:transform lg:translate-x-full"
@after-leave="onTransitionLeave">
<form v-if="opened"
class="justify-self-end flex flex-col bg-base-100 text-base-content lg:w-3/4 w-full h-full overflow-auto p-2"
@click.stop @submit.prevent="onSaveUser">
<h2 class="font-sans text-xl border-b border-accent/40 pb-2 w-full">{{ title }}</h2>
<div class="flex flex-wrap content-start ">
<div class="grow lg:basis-1/2 h-fit p-4">
<label class="label">
<span class="label-text">Login</span>
</label>
<input v-model="modelValue.login" type="text" placeholder="login"
class="input input-bordered w-full"
:class="{'input-error': errors.has('login')}" @change="onFieldChange('login')"
:disabled="isEditionMode">
<label class="label -mt-1">
<span class="label-text-alt">&nbsp;</span>
<span v-if="errors.has('login')" class="label-text-alt">{{ errors.get('login') }}</span>
</label>
<div class="grid bg-base-200 bg-opacity-60 z-30 w-full h-full absolute top-0 left-0 overflow-hidden"
@click="opened = false">
<Transition
enter-active-class="lg:duration-300 ease-in-out"
enter-from-class="lg:transform lg:translate-x-full"
enter-to-class="lg:translate-x-0"
leave-active-class="lg:duration-300 ease-in-out"
leave-from-class="lg:translate-x-0"
leave-to-class="lg:transform lg:translate-x-full"
@after-leave="onTransitionLeave">
<form v-if="opened"
class="justify-self-end flex flex-col bg-base-100 text-base-content lg:w-3/4 w-full h-full overflow-auto p-2"
@click.stop @submit.prevent="onSaveUser">
<h2 class="font-sans text-xl border-b border-accent/40 pb-2 w-full">{{ title }}</h2>
<div class="flex flex-wrap content-start ">
<div class="grow lg:basis-1/2 h-fit p-4">
<label class="label">
<span class="label-text">Login</span>
</label>
<input v-model="modelValue.login" type="text" placeholder="login"
class="input input-bordered w-full"
:class="{'input-error': errors.has('login')}" @change="onFieldChange('login')"
:disabled="isEditionMode">
<label class="label -mt-1">
<span class="label-text-alt">&nbsp;</span>
<span v-if="errors.has('login')" class="label-text-alt">{{ errors.get('login') }}</span>
</label>

<label class="label -mt-6">
<span class="label-text">Username</span>
</label>
<input v-model="modelValue.name" type="text" placeholder="username"
class="input input-bordered w-full"
:class="{'input-error': errors.has('name')}" @change="onFieldChange('name')">
<label class="label -mt-1">
<span class="label-text-alt">&nbsp;</span>
<span v-if="errors.has('name')" class="label-text-alt">{{ errors.get('name') }}</span>
</label>
<label class="label -mt-6">
<span class="label-text">Username</span>
</label>
<input v-model="modelValue.name" type="text" placeholder="username"
class="input input-bordered w-full"
:class="{'input-error': errors.has('name')}" @change="onFieldChange('name')">
<label class="label -mt-1">
<span class="label-text-alt">&nbsp;</span>
<span v-if="errors.has('name')" class="label-text-alt">{{ errors.get('name') }}</span>
</label>

<label class="label -mt-6">
<span class="label-text">Mail</span>
</label>
<input v-model="modelValue.mail" type="email" placeholder="mail address"
class="input input-bordered w-full"
:class="{'input-error': errors.has('mail')}" @change="onFieldChange('mail')"
:disabled="isEditionMode">
<label class="label -mt-1">
<span class="label-text-alt">&nbsp;</span>
<span v-if="errors.has('mail')" class="label-text-alt">{{ errors.get('mail') }}</span>
</label>
</div>
<div class="grow lg:basis-1/2 h-fit p-4">
<label class="label">
<span class="label-text">Password</span>
</label>
<input v-model="modelValue.password" type="password" class="input input-bordered w-full"
:class="{'input-error': errors.has('password')}" @change="onFieldChange('password')">
<label class="label -mt-1">
<span class="label-text-alt">&nbsp;</span>
<span v-if="errors.has('password')" class="label-text-alt">{{
errors.get('password')
}}</span>
</label>
<label class="label -mt-6">
<span class="label-text">Mail</span>
</label>
<input v-model="modelValue.mail" type="email" placeholder="mail address"
class="input input-bordered w-full"
:class="{'input-error': errors.has('mail')}" @change="onFieldChange('mail')"
:disabled="isEditionMode">
<label class="label -mt-1">
<span class="label-text-alt">&nbsp;</span>
<span v-if="errors.has('mail')" class="label-text-alt">{{ errors.get('mail') }}</span>
</label>
</div>
<div class="grow lg:basis-1/2 h-fit p-4">
<label class="label">
<span class="label-text">Password</span>
</label>
<input v-model="modelValue.password" type="password" class="input input-bordered w-full"
:class="{'input-error': errors.has('password')}" @change="onFieldChange('password')">
<label class="label -mt-1">
<span class="label-text-alt">&nbsp;</span>
<span v-if="errors.has('password')" class="label-text-alt">{{
errors.get('password')
}}</span>
</label>

<label class="label -mt-6">
<span class="label-text">Password Confirmation</span>
</label>
<input v-model="passwordConfirm" type="password" class="input input-bordered w-full"
:class="{'input-error': errors.has('confirm')}" @change="onFieldChange('confirm')">
<label class="label -mt-1">
<span class="label-text-alt">&nbsp;</span>
<span v-if="errors.has('confirm')" class="label-text-alt">{{ errors.get('confirm') }}</span>
</label>
</div>
<div class="grow lg:basis-1/2 h-fit p-4 border-error rounded-lg"
:class="{'border': errors.has('roles')}">
<UserRoleInput :model-value="modelValue.roles" @update:modelValue="onRoleUpdate"/>
<span v-if="errors.has('roles')" class="label-text-alt">{{ errors.get('roles') }}</span>
</div>
</div>
<span class="grow"></span>
<div>
<button class="btn m-2" @click.prevent.stop="onCancel">Annuler</button>
<button class="btn btn-primary m-2" @click.prevent.stop="onSaveUser" :disabled="!hasValidRoles">
Enregistrer
</button>
</div>
</form>
</Transition>
<label class="label -mt-6">
<span class="label-text">Password Confirmation</span>
</label>
<input v-model="passwordConfirm" type="password" class="input input-bordered w-full"
:class="{'input-error': errors.has('confirm')}" @change="onFieldChange('confirm')">
<label class="label -mt-1">
<span class="label-text-alt">&nbsp;</span>
<span v-if="errors.has('confirm')" class="label-text-alt">{{ errors.get('confirm') }}</span>
</label>
</div>
<div class="grow lg:basis-1/2 h-fit p-4 border-error rounded-lg"
:class="{'border': errors.has('roles')}">
<UserRoleInput :model-value="modelValue.roles" @update:modelValue="onRoleUpdate"/>
<span v-if="errors.has('roles')" class="label-text-alt">{{ errors.get('roles') }}</span>
</div>
</div>
<span class="grow"></span>
<div>
<button class="btn m-2" @click.prevent.stop="onCancel">Annuler</button>
<button class="btn btn-primary m-2" @click.prevent.stop="onSaveUser" :disabled="!hasValidRoles">
Enregistrer
</button>
</div>
</form>
</Transition>

</div>
</div>
</template>

<script lang="ts">
import {Component, Prop, Vue} from "vue-facing-decorator";
import {User} from "@/security/model/User";
import UserRoleInput from "@/administration/component/usereditor/UserRoleInput.vue";
import { Component, Prop, Vue } from 'vue-facing-decorator';
import { User } from '@/security/model/User';
import UserRoleInput from '@/administration/component/usereditor/UserRoleInput.vue';
import { MAIL_PATTERN, ULID_PATTERN } from '@/common/services/RegexPattern';
const CANCEL_EVENT: string = 'cancel';
const SUBMIT_EVENT: string = 'submit';
const CHANGE_EVENT: string = 'change';
const MAIL_PATTERN = /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/;
const ULID_PATTERN = /^[A-Z]*(:[A-Z]{2}[0-7][0-9A-HJKMNP-TV-Z]{25})?$/;
@Component({
name: 'UserEditor',
components: {UserRoleInput},
emits: [CANCEL_EVENT, SUBMIT_EVENT, CHANGE_EVENT],
name: 'UserEditor',
components: { UserRoleInput },
emits: [CANCEL_EVENT, SUBMIT_EVENT, CHANGE_EVENT],
})
export default class UserEditor extends Vue {
@Prop() private modelValue: User;
private passwordConfirm: string = '';
private title = 'Create new user';
private isEditionMode: boolean = false;
private errors: Map<string, string> = new Map<string, string>();
private opened: boolean = false;
private closeEvent: typeof SUBMIT_EVENT | typeof CANCEL_EVENT = CANCEL_EVENT;
@Prop() private modelValue: User;
private passwordConfirm: string = '';
private title = 'Create new user';
private isEditionMode: boolean = false;
private errors: Map<string, string> = new Map<string, string>();
private opened: boolean = false;
private closeEvent: typeof SUBMIT_EVENT | typeof CANCEL_EVENT = CANCEL_EVENT;
mounted(): void {
this.isEditionMode = '_id' in this.modelValue && this.modelValue._id !== undefined;
this.title = this.isEditionMode ? `Update user ${this.modelValue.login}` : 'Create new user';
this.$nextTick(() => this.opened = true);
}
private onFieldChange(field: string): void {
this.errors.delete(field);
}
mounted(): void {
this.isEditionMode = '_id' in this.modelValue && this.modelValue._id !== undefined;
this.title = this.isEditionMode ? `Update user ${this.modelValue.login}` : 'Create new user';
this.$nextTick(() => this.opened = true);
}
private onCancel(): void {
this.closeEvent = CANCEL_EVENT;
this.opened = false;
}
private onFieldChange(field: string): void {
this.errors.delete(field);
}
private onSaveUser(): void {
if (!this.modelValue.login) {
this.errors.set('login', 'Login is mandatory !');
}
if (!this.modelValue.name) {
this.errors.set('name', 'Name is mandatory !');
}
if (!this.modelValue.mail) {
this.errors.set('mail', 'Mail address is mandatory !');
} else if (!MAIL_PATTERN.test(this.modelValue.mail)) {
this.errors.set('mail', 'Mail address must be syntactically correct !');
}
if (!this.modelValue.roles || this.modelValue.roles.length === 0) {
this.errors.set('roles', 'Role is mandatory !');
}
if (!this.hasValidRoles) {
this.errors.set('roles', `All role scope must match ${ULID_PATTERN}`);
}
if (!this.isEditionMode) {
if (!this.modelValue.password) {
this.errors.set('password', 'Password is mandatory !');
} else if (this.modelValue.password !== this.passwordConfirm) {
this.errors.set('confirm', "Password confirmation doesn't match !");
}
}
private onCancel(): void {
this.closeEvent = CANCEL_EVENT;
this.opened = false;
}
if (this.errors.size === 0) {
this.closeEvent = SUBMIT_EVENT;
this.opened = false;
}
private onSaveUser(): void {
if (!this.modelValue.login) {
this.errors.set('login', 'Login is mandatory !');
}
private onRoleUpdate(event: Event): void {
this.errors.delete('roles');
this.modelValue.roles.splice(0, this.modelValue.roles.length, ...event);
if (!this.modelValue.name) {
this.errors.set('name', 'Name is mandatory !');
}
if (!this.modelValue.mail) {
this.errors.set('mail', 'Mail address is mandatory !');
} else if (!MAIL_PATTERN.test(this.modelValue.mail)) {
this.errors.set('mail', 'Mail address must be syntactically correct !');
}
if (!this.modelValue.roles || this.modelValue.roles.length === 0) {
this.errors.set('roles', 'Role is mandatory !');
}
if (!this.hasValidRoles) {
this.errors.set('roles', `All role scope must match ${ULID_PATTERN}`);
}
if (!this.isEditionMode) {
if (!this.modelValue.password) {
this.errors.set('password', 'Password is mandatory !');
} else if (this.modelValue.password !== this.passwordConfirm) {
this.errors.set('confirm', 'Password confirmation doesn\'t match !');
}
}
get hasValidRoles(): boolean {
return this.modelValue.roles.findIndex(r => !ULID_PATTERN.test(r)) == -1;
if (this.errors.size === 0) {
this.closeEvent = SUBMIT_EVENT;
this.opened = false;
}
}
private onRoleUpdate(event: Event): void {
this.errors.delete('roles');
this.modelValue.roles.splice(0, this.modelValue.roles.length, ...event);
}
get hasValidRoles(): boolean {
return this.modelValue.roles.findIndex(r => !ULID_PATTERN.test(r)) == -1;
}
private onTransitionLeave(): void {
if (this.closeEvent === CANCEL_EVENT) {
this.$emit(this.closeEvent);
} else {
this.$emit(this.closeEvent, this.modelValue);
}
private onTransitionLeave(): void {
if (this.closeEvent === CANCEL_EVENT) {
this.$emit(this.closeEvent);
} else {
this.$emit(this.closeEvent, this.modelValue);
}
}
}
</script>
3 changes: 3 additions & 0 deletions seaside/src/common/services/RegexPattern.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const URL_PATTERN = /^(((https?):\/\/)(%[0-9A-Fa-f]{2}|[-()_.!~*';/?:@&=+$,A-Za-z0-9])+)([).!';/?:,][[:blank:]])?$/;
export const MAIL_PATTERN = /^[a-zA-Z0-9_+&*-]+(?:\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,7}$/;
export const ULID_PATTERN = /^[A-Z]*(:[A-Z]{2}[0-7][0-9A-HJKMNP-TV-Z]{25})?$/;
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ import { Observable, Subject } from 'rxjs';
import ModalWindow from '@/common/components/ModalWindow.vue';
import TagInput from '@/common/components/TagInput.vue';
import tagsService from '@/techwatch/services/TagsService';
import feedService, { URL_PATTERN } from '@/configuration/services/FeedService';
import feedService from '@/configuration/services/FeedService';
import { URL_PATTERN } from '@/common/services/RegexPattern';
@Component({
name: 'FeedEditor',
Expand Down
Loading

0 comments on commit 461f1b7

Please sign in to comment.