CVE-2023-0643 (published 2023-02-02T12:15Z)

Improper Handling of Additional Special Element in GitHub repository squidex/squidex prior to 7.4.0.

CVSS v3 base score: 6.1, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N)

Weakness information:
cwe_id: CWE-167
cwe_name: Improper Handling of Additional Special Element
description: The product receives input from an upstream component, but it does not handle or incorrectly handles when an additional unexpected special element is provided.
extended_description: 
            
            
         
         

Fix information:
repo_url: https://github.com/squidex/squidex
commit_details: repo_url: https://github.com/squidex/squidex
author: Sebastian Stehle
author_date: 2022-12-13 13:19:46+01:00
msg: Escape HTML in markdown. (#955)

* Escape HTML in markdown.

* Escape variables for message history.

* Rename escape method.
repository: repo_url: https://github.com/squidex/squidex
repo_name: Squidex/squidex
description: Headless CMS and Content Managment Hub
repo_language: C#
owner: squidex
file_changes: filename: declarations.d.ts
change_type: ModificationType.MODIFY
diff: @@ -18,3 +18,7 @@ declare module 'sortablejs' {
 
     export function create(element: any, options: any): Ref;
 }
+
+declare namespace marked {
+    export function escape(input: string): string;
+}
\ No newline at end of file

programming_language: TypeScript
filename: rule-page.component.ts
change_type: ModificationType.MODIFY
diff: @@ -10,7 +10,7 @@ import { AbstractControl } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { debounceTime, Subscription } from 'rxjs';
 import { ActionForm, ALL_TRIGGERS, MessageBus, ResourceOwner, RuleDto, RuleElementDto, RulesService, RulesState, SchemasState, TriggerForm, value$ } from '@app/shared';
-import { RuleConfigured } from '../messages';
+import { RuleConfigured } from './../messages';
 
 @Component({
     selector: 'sqx-rule-page',

programming_language: TypeScript
filename: rule-simulator-page.component.ts
change_type: ModificationType.MODIFY
diff: @@ -8,7 +8,7 @@
 import { Component, OnInit } from '@angular/core';
 import { ActivatedRoute } from '@angular/router';
 import { MessageBus, ResourceOwner, RuleSimulatorState, SimulatedRuleEventDto } from '@app/shared';
-import { RuleConfigured } from '../messages';
+import { RuleConfigured } from './../messages';
 
 @Component({
     selector: 'sqx-simulator-events-page',

programming_language: TypeScript
filename: generic-action.component.html
change_type: ModificationType.MODIFY
diff: @@ -47,7 +47,7 @@
             </ng-container>
 
             <sqx-form-hint>
-                <span [sqxMarkdown]="property.description" [inline]="true" [html]="true"></span>
+                <span [sqxMarkdown]="property.description" [inline]="true"></span>
 
                 <div *ngIf="property.isFormattable">
                     {{ 'rules.advancedFormattingHint' | sqxTranslate }}: <a tabindex="-1" href="https://docs.squidex.io/concepts/rules#3-formatting" sqxExternalLink>{{ 'common.documentation' | sqxTranslate }}</a>

programming_language: HTML
filename: markdown.directive.spec.ts
change_type: ModificationType.ADD
diff: @@ -0,0 +1,89 @@
+/*
+ * Squidex Headless CMS
+ *
+ * @license
+ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
+ */
+
+import { Renderer2 } from '@angular/core';
+import { IMock, It, Mock, Times } from 'typemoq';
+import { MarkdownDirective } from './markdown.directive';
+
+describe('MarkdownDirective', () => {
+    let renderer: IMock<Renderer2>;
+    let markdownElement = {};
+    let markdownDirective: MarkdownDirective;
+
+    beforeEach(() => {
+        renderer = Mock.ofType<Renderer2>();
+
+        markdownElement = {};
+        markdownDirective = new MarkdownDirective(markdownElement as any, renderer.object);
+    });
+
+    it('should render empty text as text', () => {
+        markdownDirective.markdown = '';
+        markdownDirective.ngOnChanges();
+
+        verifyTextRender('');
+    });
+
+    it('should render as text if result has no tags', () => {
+        markdownDirective.inline = true;
+        markdownDirective.markdown = 'markdown';
+        markdownDirective.ngOnChanges();
+
+        verifyTextRender('markdown');
+    });
+
+    it('should render as text if optional', () => {
+        markdownDirective.optional = true;
+        markdownDirective.markdown = '**bold**';
+        markdownDirective.ngOnChanges();
+
+        verifyTextRender('**bold**');
+    });
+
+    it('should render if optional with exclamation', () => {
+        markdownDirective.optional = true;
+        markdownDirective.markdown = '!**bold**';
+        markdownDirective.ngOnChanges();
+
+        verifyHtmlRender('<strong>bold</strong>');
+    });
+
+    it('should render as HTML if allowed', () => {
+        markdownDirective.inline = false;
+        markdownDirective.markdown = '**bold**';
+        markdownDirective.ngOnChanges();
+
+        verifyHtmlRender('<p><strong>bold</strong></p>\n');
+    });
+
+    it('should render as inline HTML if allowed', () => {
+        markdownDirective.markdown = '!**bold**';
+        markdownDirective.ngOnChanges();
+
+        verifyHtmlRender('<strong>bold</strong>');
+    });
+
+    it('should render HTML escaped', () => {
+        markdownDirective.inline = false;
+        markdownDirective.markdown = '<h1>Header</h1>';
+        markdownDirective.ngOnChanges();
+
+        verifyHtmlRender('<p>&lt;h1&gt;Header&lt;/h1&gt;</p>\n');
+    });
+
+    function verifyTextRender(text: string) {
+        renderer.verify(x => x.setProperty(It.isAny(), 'textContent', text), Times.once());
+
+        expect().nothing();
+    }
+
+    function verifyHtmlRender(text: string) {
+        renderer.verify(x => x.setProperty(It.isAny(), 'innerHTML', text), Times.once());
+
+        expect().nothing();
+    }
+});
\ No newline at end of file

programming_language: TypeScript
filename: markdown.directive.ts
change_type: ModificationType.MODIFY
diff: @@ -6,24 +6,7 @@
  */
 
 import { Directive, ElementRef, Input, OnChanges, Renderer2 } from '@angular/core';
-import { marked } from 'marked';
-
-const RENDERER_DEFAULT = new marked.Renderer();
-const RENDERER_INLINE = new marked.Renderer();
-
-RENDERER_DEFAULT.link = (href, _, text) => {
-    if (href && href.startsWith('mailto')) {
-        return text;
-    } else {
-        return `<a href="${href}" target="_blank", rel="noopener">${text} <i class="icon-external-link"></i></a>`;
-    }
-};
-
-RENDERER_INLINE.paragraph = (text) => {
-    return text;
-};
-
-RENDERER_INLINE.link = RENDERER_DEFAULT.link;
+import { renderMarkdown } from '@app/framework/internal';
 
 @Directive({
     selector: '[sqxMarkdown]',
@@ -35,9 +18,6 @@ export class MarkdownDirective implements OnChanges {
     @Input()
     public inline = true;
 
-    @Input()
-    public html = false;
-
     @Input()
     public optional = false;
 
@@ -50,22 +30,28 @@ export class MarkdownDirective implements OnChanges {
     public ngOnChanges() {
         let html = '';
 
-        const markdown = this.markdown;
+        let markdown = this.markdown;
+
+        const hasExclamation = markdown.indexOf('!') === 0;
+
+        if (hasExclamation) {
+            markdown = markdown.substring(1);
+        }
 
         if (!markdown) {
             html = markdown;
-        } else if (this.optional && markdown.indexOf('!') !== 0) {
+        } else if (this.optional && !hasExclamation) {
             html = markdown;
         } else if (this.markdown) {
-            const renderer = this.inline ? RENDERER_INLINE : RENDERER_DEFAULT;
-
-            html = marked(this.markdown, { renderer });
+            html = renderMarkdown(markdown, this.inline);
         }
 
-        if (!this.html && (!html || html === this.markdown || html.indexOf('<') < 0)) {
-            this.renderer.setProperty(this.element.nativeElement, 'textContent', html);
-        } else {
+        const hasHtml = html.indexOf('<') >= 0;
+
+        if (hasHtml) {
             this.renderer.setProperty(this.element.nativeElement, 'innerHTML', html);
+        } else {
+            this.renderer.setProperty(this.element.nativeElement, 'textContent', html);
         }
     }
 }

programming_language: TypeScript
filename: pager.component.ts
change_type: ModificationType.MODIFY
diff: @@ -6,7 +6,7 @@
  */
 
 import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
-import { PagingInfo } from '../state';
+import { PagingInfo } from './../state';
 
 export const PAGE_SIZES: ReadonlyArray<number> = [10, 20, 30, 50];
 

programming_language: TypeScript
filename: markdown.pipe.spec.ts
change_type: ModificationType.MODIFY
diff: @@ -8,21 +8,29 @@
 import { MarkdownInlinePipe, MarkdownPipe } from './markdown.pipe';
 
 describe('MarkdownInlinePipe', () => {
+    const pipe = new MarkdownInlinePipe();
+
     it('should convert link to html', () => {
-        const actual = new MarkdownInlinePipe().transform('[link-name](link-url)');
+        const actual = pipe.transform('[link-name](link-url)');
 
         expect(actual).toBe('<a href="link-url" target="_blank", rel="noopener">link-name <i class="icon-external-link"></i></a>');
     });
 
     it('should convert markdown to html', () => {
-        const actual = new MarkdownInlinePipe().transform('*bold*');
+        const actual = pipe.transform('*bold*');
 
         expect(actual).toBe('<em>bold</em>');
     });
 
+    it('should escape input html', () => {
+        const actual = pipe.transform('<h1>Header</h1>');
+
+        expect(actual).toBe('&lt;h1&gt;Header&lt;/h1&gt;');
+    });
+
     [null, undefined, ''].forEach(x => {
         it('should return empty string for invalid value', () => {
-            const actual = new MarkdownInlinePipe().transform(x);
+            const actual = pipe.transform(x);
 
             expect(actual).toBe('');
         });
@@ -30,21 +38,29 @@ describe('MarkdownInlinePipe', () => {
 });
 
 describe('MarkdownPipe', () => {
+    const pipe = new MarkdownPipe();
+
     it('should convert link to html', () => {
-        const actual = new MarkdownPipe().transform('[link-name](link-url)');
+        const actual = pipe.transform('[link-name](link-url)');
 
         expect(actual).toBe('<p><a href="link-url" target="_blank", rel="noopener">link-name <i class="icon-external-link"></i></a></p>\n');
     });
 
     it('should convert markdown to html', () => {
-        const actual = new MarkdownPipe().transform('*bold*');
+        const actual = pipe.transform('*bold*');
 
         expect(actual).toBe('<p><em>bold</em></p>\n');
     });
 
+    it('should escape input html', () => {
+        const actual = pipe.transform('<h1>Header</h1>');
+
+        expect(actual).toBe('<p>&lt;h1&gt;Header&lt;/h1&gt;</p>\n');
+    });
+
     [null, undefined, ''].forEach(x => {
         it('should return empty string for invalid value', () => {
-            const actual = new MarkdownPipe().transform(x);
+            const actual = pipe.transform(x);
 
             expect(actual).toBe('');
         });

programming_language: TypeScript
filename: markdown.pipe.ts
change_type: ModificationType.MODIFY
diff: @@ -6,25 +6,7 @@
  */
 
 import { Pipe, PipeTransform } from '@angular/core';
-import { marked } from 'marked';
-
-const renderer = new marked.Renderer();
-
-renderer.link = (href, _, text) => {
-    if (href && href.startsWith('mailto')) {
-        return text;
-    } else {
-        return `<a href="${href}" target="_blank", rel="noopener">${text} <i class="icon-external-link"></i></a>`;
-    }
-};
-
-const inlinerRenderer = new marked.Renderer();
-
-inlinerRenderer.paragraph = (text) => {
-    return text;
-};
-
-inlinerRenderer.link = renderer.link;
+import { renderMarkdown } from '@app/framework/internal';
 
 @Pipe({
     name: 'sqxMarkdown',
@@ -32,11 +14,7 @@ inlinerRenderer.link = renderer.link;
 })
 export class MarkdownPipe implements PipeTransform {
     public transform(text: string | undefined | null): string {
-        if (text) {
-            return marked(text, { renderer });
-        } else {
-            return '';
-        }
+        return renderMarkdown(text, false);
     }
 }
 
@@ -46,10 +24,6 @@ export class MarkdownPipe implements PipeTransform {
 })
 export class MarkdownInlinePipe implements PipeTransform {
     public transform(text: string | undefined | null): string {
-        if (text) {
-            return marked(text, { renderer: inlinerRenderer });
-        } else {
-            return '';
-        }
+        return renderMarkdown(text, true);
     }
-}
+}
\ No newline at end of file

programming_language: TypeScript
filename: internal.ts
change_type: ModificationType.MODIFY
diff: @@ -33,6 +33,7 @@ export * from './utils/hateos';
 export * from './utils/interpolator';
 export * from './utils/keys';
 export * from './utils/math-helper';
+export * from './utils/markdown';
 export * from './utils/modal-positioner';
 export * from './utils/modal-view';
 export * from './utils/picasso';

programming_language: TypeScript
filename: localizer.service.ts
change_type: ModificationType.MODIFY
diff: @@ -6,7 +6,7 @@
  */
 
 import { Injectable } from '@angular/core';
-import { compareStrings } from '../utils/array-helper';
+import { compareStrings } from './../utils/array-helper';
 
 @Injectable()
 export class LocalizerService {

programming_language: TypeScript
filename: date-time.spec.ts
change_type: ModificationType.MODIFY
diff: @@ -5,7 +5,7 @@
  * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
  */
 
-import { DateHelper } from '..';
+import { DateHelper } from './..';
 import { DateTime } from './date-time';
 
 describe('DateTime', () => {

programming_language: TypeScript
filename: markdown.ts
change_type: ModificationType.ADD
diff: @@ -0,0 +1,61 @@
+/*
+ * Squidex Headless CMS
+ *
+ * @license
+ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
+ */
+
+import { marked } from 'marked';
+
+function renderLink(href: string, _: string, text: string) {
+    if (href && href.startsWith('mailto')) {
+        return text;
+    } else {
+        return `<a href="${href}" target="_blank", rel="noopener">${text} <i class="icon-external-link"></i></a>`;
+    }
+}
+
+function renderInlineParagraph(text: string) {
+    return text;
+}
+
+const RENDERER_DEFAULT = new marked.Renderer();
+const RENDERER_INLINE = new marked.Renderer();
+
+RENDERER_INLINE.paragraph = renderInlineParagraph;
+RENDERER_INLINE.link = renderLink;
+RENDERER_DEFAULT.link = renderLink;
+
+export function renderMarkdown(input: string | undefined | null, inline: boolean) {
+    if (!input) {
+        return '';
+    }
+
+    input = escapeHTML(input);
+
+    if (inline) {
+        return marked(input, { renderer: RENDERER_INLINE });
+    } else {
+        return marked(input, { renderer: RENDERER_DEFAULT });
+    }
+}
+
+const escapeTestNoEncode = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/;
+const escapeReplaceNoEncode = new RegExp(escapeTestNoEncode.source, 'g');
+const escapeReplacements = {
+  '&' : '&amp;',
+  '<' : '&lt;',
+  '>' : '&gt;',
+  '"' : '&quot;',
+  '\'': '&#39;',
+};
+
+const getEscapeReplacement = (ch: string) => escapeReplacements[ch];
+
+export function escapeHTML(html: string) {
+    if (escapeTestNoEncode.test(html)) {
+        return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
+    }
+
+    return html;
+}
\ No newline at end of file

programming_language: TypeScript
filename: text-measurer.ts
change_type: ModificationType.MODIFY
diff: @@ -6,7 +6,7 @@
  */
 
 import { ElementRef } from '@angular/core';
-import { Types } from '../internal';
+import { Types } from './../internal';
 
 let CANVAS: HTMLCanvasElement | null = null;
 

programming_language: TypeScript
filename: history.service.spec.ts
change_type: ModificationType.MODIFY
diff: @@ -7,7 +7,71 @@
 
 import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
 import { inject, TestBed } from '@angular/core/testing';
-import { ApiUrlConfig, DateTime, HistoryEventDto, HistoryService, Version } from '@app/shared/internal';
+import { firstValueFrom, of } from 'rxjs';
+import { IMock, Mock } from 'typemoq';
+import { ApiUrlConfig, DateTime, formatHistoryMessage, HistoryEventDto, HistoryService, UsersProviderService, Version } from '@app/shared/internal';
+
+describe('formatHistoryMessage', () => {
+    let userProvider: IMock<UsersProviderService>;
+
+    beforeEach(() => {
+        userProvider = Mock.ofType<UsersProviderService>();
+    });
+
+    it('should provide simple message', async () => {
+        const message = await firstValueFrom(formatHistoryMessage('message', userProvider.object));
+
+        expect(message).toEqual('message');
+    });
+
+    it('should embed marker', async () => {
+        const message = await firstValueFrom(formatHistoryMessage('{Name}', userProvider.object));
+
+        expect(message).toEqual('<span class="marker-ref">Name</span>');
+    });
+
+    it('should embed marker ref and escape HTML', async () => {
+        const message = await firstValueFrom(formatHistoryMessage('{<h1>HTML</h1>}', userProvider.object));
+
+        expect(message).toEqual('<span class="marker-ref">&lt;h1&gt;HTML&lt;/h1&gt;</span>');
+    });
+
+    it('should embed user ref with unknown type', async () => {
+        const message = await firstValueFrom(formatHistoryMessage('{unknown:User1}', userProvider.object));
+
+        expect(message).toEqual('<span class="user-ref">User1</span>');
+    });
+
+    it('should embed user ref with client', async () => {
+        const message = await firstValueFrom(formatHistoryMessage('{user:unknown:Client1}', userProvider.object));
+
+        expect(message).toEqual('<span class="user-ref">Client1-client</span>');
+    });
+
+    it('should embed user ref with client ending with client', async () => {
+        const message = await firstValueFrom(formatHistoryMessage('{user:client:Sample-Client}', userProvider.object));
+
+        expect(message).toEqual('<span class="user-ref">Sample-Client</span>');
+    });
+
+    it('should embed user ref with subject', async () => {
+        userProvider.setup(x => x.getUser('1', null))
+            .returns(() => of({ id: '1', displayName: 'User1' }));
+
+        const message = await firstValueFrom(formatHistoryMessage('{user:subject:1}', userProvider.object));
+
+        expect(message).toEqual('<span class="user-ref">User1</span>');
+    });
+
+    it('should embed user ref with id', async () => {
+        userProvider.setup(x => x.getUser('1', null))
+            .returns(() => of({ id: '1', displayName: 'User1' }));
+
+        const message = await firstValueFrom(formatHistoryMessage('{user:1}', userProvider.object));
+
+        expect(message).toEqual('<span class="user-ref">User1</span>');
+    });
+});
 
 describe('HistoryService', () => {
     beforeEach(() => {

programming_language: TypeScript
filename: history.service.ts
change_type: ModificationType.MODIFY
diff: @@ -7,9 +7,9 @@
 
 import { HttpClient, HttpHeaders } from '@angular/common/http';
 import { Injectable } from '@angular/core';
-import { Observable, of } from 'rxjs';
+import { firstValueFrom, from, Observable } from 'rxjs';
 import { map } from 'rxjs/operators';
-import { ApiUrlConfig, DateTime, pretifyError, Version } from '@app/framework';
+import { ApiUrlConfig, DateTime, escapeHTML, pretifyError, Version } from '@app/framework';
 import { UsersProviderService } from './users-provider.service';
 
 export class HistoryEventDto {
@@ -24,43 +24,50 @@ export class HistoryEventDto {
     }
 }
 
-const REPLACEMENT_TEMP = '$TEMP$';
-
 export function formatHistoryMessage(message: string, users: UsersProviderService): Observable<string> {
-    const userName = (userId: string) => {
-        const parts = userId.split(':');
-
-        if (parts.length === 1) {
-            return users.getUser(parts[0], null).pipe(map(u => u.displayName));
-        } else if (parts[0] === 'subject') {
-            return users.getUser(parts[1], null).pipe(map(u => u.displayName));
-        } else if (parts[1].endsWith('client')) {
-            return of(parts[1]);
-        } else {
-            return of(`${parts[1]}-client`);
-        }
-    };
+    async function getUserName(id: string): Promise<string> {
+        const user = await firstValueFrom(users.getUser(id, null));
 
-    let foundUserId: string | null = null;
+        return user.displayName;
+    }
 
-    message = message.replace(/{([^\s:]*):([^}]*)}/, (match: string, type: string, id: string) => {
-        if (type === 'user') {
-            foundUserId = id;
-            return REPLACEMENT_TEMP;
-        } else {
-            return id;
+    async function format(message: string): Promise<string> {
+        const placeholderMatches = message.matchAll(/{(?<type>[^\s:]*):(?<id>[^}]*)}/g) || [];
+        const placeholderValues: string[] = [];
+
+        for (const match of placeholderMatches) {
+            const { id, type } = match.groups!;
+
+            if (type !== 'user') {
+                placeholderValues.push(id);
+                continue;
+            }
+
+            const parts = id.split(':');
+
+            if (parts.length === 1) {
+                placeholderValues.push(await getUserName(parts[0]));
+            } else if (parts[0] === 'subject') {
+                placeholderValues.push(await getUserName(parts[1]));
+            } else if (parts[1].toLowerCase().endsWith('client')) {
+                placeholderValues.push(parts[1]);
+            } else {
+                placeholderValues.push(`${parts[1]}-client`);
+            }
         }
-    });
 
-    message = message.replace(/{([^}]*)}/g, (match: string, marker: string) => {
-        return `<span class="marker-ref">${marker}</span>`;
-    });
+        message = message.replace(/{([^\s:]*):([^}]*)}/, () => {
+            return `<span class="user-ref">${escapeHTML(placeholderValues.shift() || '')}</span>`;
+        });
+
+        message = message.replace(/{([^}]*)}/g, (match: string, marker: string) => {
+            return `<span class="marker-ref">${escapeHTML(marker)}</span>`;
+        });
 
-    if (foundUserId) {
-        return userName(foundUserId).pipe(map(t => message.replace(REPLACEMENT_TEMP, `<span class="user-ref">${t}</span>`)));
+        return message;
     }
 
-    return of(message);
+    return from(format(message));
 }
 
 @Injectable()

programming_language: TypeScript
filename: rules.service.spec.ts
change_type: ModificationType.MODIFY
diff: @@ -8,7 +8,7 @@
 import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
 import { inject, TestBed } from '@angular/core/testing';
 import { ApiUrlConfig, DateTime, Resource, ResourceLinks, RuleDto, RuleElementDto, RuleElementPropertyDto, RuleEventDto, RuleEventsDto, RulesDto, RulesService, Version } from '@app/shared/internal';
-import { RuleCompletions } from '..';
+import { RuleCompletions } from './..';
 import { SimulatedRuleEventDto, SimulatedRuleEventsDto } from './rules.service';
 
 describe('RulesService', () => {

programming_language: TypeScript
filename: schemas.service.spec.ts
change_type: ModificationType.MODIFY
diff: @@ -8,7 +8,7 @@
 import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
 import { inject, TestBed } from '@angular/core/testing';
 import { ApiUrlConfig, createProperties, DateTime, FieldRule, NestedFieldDto, Resource, ResourceLinks, RootFieldDto, SchemaDto, SchemaPropertiesDto, SchemasDto, SchemasService, Version } from '@app/shared/internal';
-import { SchemaCompletions } from '..';
+import { SchemaCompletions } from './..';
 
 describe('SchemasService', () => {
     const version = new Version('1');

programming_language: TypeScript
filename: asset-scripts.state.spec.ts
change_type: ModificationType.MODIFY
diff: @@ -9,8 +9,8 @@ import { of, throwError } from 'rxjs';
 import { onErrorResumeNext } from 'rxjs/operators';
 import { IMock, It, Mock, Times } from 'typemoq';
 import { DialogService, versioned } from '@app/shared/internal';
-import { AppsService, AssetScriptsPayload } from '../services/apps.service';
-import { createAssetScripts } from '../services/apps.service.spec';
+import { AppsService, AssetScriptsPayload } from './../services/apps.service';
+import { createAssetScripts } from './../services/apps.service.spec';
 import { TestValues } from './_test-helpers';
 import { AssetScriptsState } from './asset-scripts.state';
 

programming_language: TypeScript
filename: asset-scripts.state.ts
change_type: ModificationType.MODIFY
diff: @@ -9,7 +9,7 @@ import { Injectable } from '@angular/core';
 import { Observable } from 'rxjs';
 import { finalize, tap } from 'rxjs/operators';
 import { DialogService, LoadingState, Resource, shareSubscribed, State, Version } from '@app/framework';
-import { AppsService, AssetScripts, AssetScriptsPayload } from '../services/apps.service';
+import { AppsService, AssetScripts, AssetScriptsPayload } from './../services/apps.service';
 import { AppsState } from './apps.state';
 
 interface Snapshot extends LoadingState {

programming_language: TypeScript
filename: resolvers.spec.ts
change_type: ModificationType.MODIFY
diff: @@ -8,8 +8,8 @@
 import { firstValueFrom, of, throwError } from 'rxjs';
 import { IMock, Mock, Times } from 'typemoq';
 import { UIOptions } from '@app/framework';
-import { ContentsService } from '../services/contents.service';
-import { createContent } from '../services/contents.service.spec';
+import { ContentsService } from './../services/contents.service';
+import { createContent } from './../services/contents.service.spec';
 import { TestValues } from './_test-helpers';
 import { ResolveContents } from './resolvers';
 

programming_language: TypeScript
filename: rules.forms.ts
change_type: ModificationType.MODIFY
diff: @@ -7,7 +7,7 @@
 
 import { AbstractControl, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms';
 import { ExtendedFormGroup, Form, ValidatorsEx } from '@app/framework';
-import { RuleElementDto } from '../services/rules.service';
+import { RuleElementDto } from './../services/rules.service';
 
 export class ActionForm extends Form<any, UntypedFormGroup> {
     constructor(public readonly definition: RuleElementDto,

programming_language: TypeScript
filename: table-settings.spec.ts
change_type: ModificationType.MODIFY
diff: @@ -9,7 +9,7 @@ import { of } from 'rxjs';
 import { IMock, It, Mock, Times } from 'typemoq';
 import { DateTime, Version } from '@app/framework';
 import { createProperties, FieldSizes, META_FIELDS, RootFieldDto, SchemaDto, TableSettings, UIState } from '@app/shared/internal';
-import { FieldWrappings } from '..';
+import { FieldWrappings } from './..';
 
 describe('TableSettings', () => {
     let uiState: IMock<UIState>;

programming_language: TypeScript