Просмотр исходного кода

Refactor recommended-whitelist.ts to exclude restricted class attributes

Yuki Takei 1 год назад
Родитель
Сommit
93bb20d3e9

+ 29 - 0
apps/app/src/services/renderer/recommended-whitelist.spec.ts

@@ -1,3 +1,5 @@
+import { notDeepEqual } from 'assert';
+
 import { tagNames, attributes } from './recommended-whitelist';
 
 describe('recommended-whitelist', () => {
@@ -44,4 +46,31 @@ describe('recommended-whitelist', () => {
     expect(attributes.iframe).includes('src');
   });
 
+  test('.attributes.a should allow class and className by excluding partial className specification', () => {
+    expect(attributes).not.toBeNull();
+
+    assert(attributes != null);
+
+    expect(Object.keys(attributes)).includes('a');
+    expect(attributes.a).not.toContainEqual(['className', 'data-footnote-backref']);
+  });
+
+  test('.attributes.ul should allow class and className by excluding partial className specification', () => {
+    expect(attributes).not.toBeNull();
+
+    assert(attributes != null);
+
+    expect(Object.keys(attributes)).includes('a');
+    expect(attributes.a).not.toContainEqual(['className', 'data-footnote-backref']);
+  });
+
+  test('.attributes.li should allow class and className by excluding partial className specification', () => {
+    expect(attributes).not.toBeNull();
+
+    assert(attributes != null);
+
+    expect(Object.keys(attributes)).includes('a');
+    expect(attributes.a).not.toContainEqual(['className', 'data-footnote-backref']);
+  });
+
 });

+ 26 - 1
apps/app/src/services/renderer/recommended-whitelist.ts

@@ -3,6 +3,31 @@ import deepmerge from 'ts-deepmerge';
 
 type Attributes = typeof defaultSchema.attributes;
 
+type ExtractPropertyDefinition<T> = T extends Record<string, (infer U)[]>
+  ? U
+  : never;
+
+type PropertyDefinition = ExtractPropertyDefinition<NonNullable<Attributes>>;
+
+const excludeRestrictedClassAttributes = (propertyDefinitions: PropertyDefinition[]): PropertyDefinition[] => {
+  if (propertyDefinitions == null) {
+    return propertyDefinitions;
+  }
+
+  return propertyDefinitions.filter((propertyDefinition) => {
+    if (!Array.isArray(propertyDefinition)) {
+      return true;
+    }
+    return propertyDefinition[0] !== 'class' && propertyDefinition[0] !== 'className';
+  });
+};
+
+// generate relaxed schema
+const relaxedSchemaAttributes = structuredClone(defaultSchema.attributes) ?? {};
+relaxedSchemaAttributes.a = excludeRestrictedClassAttributes(relaxedSchemaAttributes.a);
+relaxedSchemaAttributes.ul = excludeRestrictedClassAttributes(relaxedSchemaAttributes.ul);
+relaxedSchemaAttributes.li = excludeRestrictedClassAttributes(relaxedSchemaAttributes.li);
+
 /**
  * reference: https://meta.stackexchange.com/questions/1777/what-html-tags-are-allowed-on-stack-exchange-sites,
  *            https://github.com/jch/html-pipeline/blob/70b6903b025c668ff3c02a6fa382031661182147/lib/html/pipeline/sanitization_filter.rb#L41
@@ -20,7 +45,7 @@ export const tagNames: Array<string> = [
 ];
 
 export const attributes: Attributes = deepmerge(
-  defaultSchema.attributes ?? {},
+  relaxedSchemaAttributes,
   {
     iframe: ['allow', 'referrerpolicy', 'sandbox', 'src', 'srcdoc'],
     video: ['controls', 'src', 'muted', 'preload', 'width', 'height', 'autoplay'],