diff --git a/web/i18n/en/settings.json b/web/i18n/en/settings.json
index 69555ab3..34116b90 100644
--- a/web/i18n/en/settings.json
+++ b/web/i18n/en/settings.json
@@ -101,5 +101,9 @@
     "advanced.debug.description": "gives you access to a page with app & device info useful for debugging.",
 
     "advanced.data": "settings data",
-    "advanced.reset": "reset all settings"
+    "advanced.reset": "reset all settings",
+    "advanced.import": "import",
+    "advanced.import.no_data": "failed loading setting data from file",
+    "advanced.import.invalid": "file does not contain valid cobalt settings",
+    "advanced.export": "export"
 }
diff --git a/web/src/components/settings/TransferSettings.svelte b/web/src/components/settings/TransferSettings.svelte
index 3db4ee8b..7ca29ca4 100644
--- a/web/src/components/settings/TransferSettings.svelte
+++ b/web/src/components/settings/TransferSettings.svelte
@@ -6,6 +6,7 @@
         updateSetting,
         loadFromString
     } from "$lib/state/settings";
+    import { validateSettings } from "$lib/settings/validate";
 
     import ActionButton from "$components/buttons/ActionButton.svelte";
 
@@ -15,17 +16,57 @@
     const updateSettings = (reader: FileReader) => {
         try {
             const data = reader.result?.toString();
-            if (!data) {
-                throw "data is missing";
-            }
+            if (!data)
+                throw $t('settings.advanced.import.no_data');
 
-            // TODO: input is not validated at all here, which means
-            //       someone can potentially import a broken config.
-            //       i don't know if we should do something about it
-            //       or just thug it out.
-            updateSetting(loadFromString(data));
+            const loadedSettings = loadFromString(data);
+            if (!validateSettings(loadedSettings))
+                throw $t('settings.advanced.import.invalid');
+
+            createDialog({
+                id: "import-confirm",
+                type: "small",
+                icon: "warn-red",
+                title: $t("dialog.safety.title"),
+                bodyText: $t("dialog.import.body"),
+                buttons: [
+                    {
+                        text: $t("dialog.button.cancel"),
+                        main: false,
+                        action: () => {},
+                    },
+                    {
+                        text: $t("dialog.button.import"),
+                        color: "red",
+                        main: true,
+                        timeout: 5000,
+                        action: () => updateSetting(loadFromString(data))
+                    },
+                ],
+            });
         } catch (e) {
-            alert(e);
+            let message;
+
+            if (e instanceof Error)
+                message = e.message;
+            else if (typeof e === 'string')
+                message = e;
+            else
+                message = $t('settings.advanced.import.no_data');
+
+            createDialog({
+                id: "settings-import-error",
+                type: "small",
+                meowbalt: "error",
+                bodyText: message,
+                buttons: [
+                    {
+                        text: $t("dialog.button.gotit"),
+                        main: true,
+                        action: () => {},
+                    },
+                ],
+            });
         }
     };
 
@@ -37,33 +78,11 @@
             const target = e.target as HTMLInputElement;
             const reader = new FileReader();
 
-            reader.onload = function () {
-                createDialog({
-                    id: "import-confirm",
-                    type: "small",
-                    icon: "warn-red",
-                    title: $t("dialog.safety.title"),
-                    bodyText: $t("dialog.import.body"),
-                    buttons: [
-                        {
-                            text: $t("dialog.button.cancel"),
-                            main: false,
-                            action: () => {},
-                        },
-                        {
-                            text: $t("dialog.button.import"),
-                            color: "red",
-                            main: true,
-                            timeout: 5000,
-                            action: () => updateSettings(reader),
-                        },
-                    ],
-                });
-            };
+            reader.onload = () => updateSettings(reader);
 
             if (target.files?.length === 1) {
                 reader.readAsText(target.files[0]);
-            } else alert("file missing");
+            }
         };
         pseudoinput.click();
     };
@@ -83,11 +102,11 @@
 
 <div class="button-row" id="settings-data-transfer">
     <ActionButton id="import-settings" click={importSettings}>
-        <IconFileImport /> import
+        <IconFileImport /> {$t("settings.advanced.import")}
     </ActionButton>
     {#if $storedSettings.schemaVersion}
     <ActionButton id="export-settings" click={exportSettings}>
-        <IconFileExport /> export
+        <IconFileExport /> {$t("settings.advanced.export")}
     </ActionButton>
     {/if}
 </div>
diff --git a/web/src/lib/settings/validate.ts b/web/src/lib/settings/validate.ts
new file mode 100644
index 00000000..9b85bcce
--- /dev/null
+++ b/web/src/lib/settings/validate.ts
@@ -0,0 +1,85 @@
+import type { Optional } from '$lib/types/generic';
+import defaultSettings from './defaults'
+import {
+    downloadModeOptions,
+    filenameStyleOptions,
+    savingMethodOptions,
+    themeOptions,
+    videoQualityOptions,
+    youtubeVideoCodecOptions,
+    type PartialSettings,
+} from '$lib/types/settings';
+
+function validateTypes(input: unknown, reference = defaultSettings as unknown) {
+    if (typeof input === 'undefined')
+        return true;
+
+    if (typeof input !== typeof reference)
+        return false;
+
+    if (typeof reference !== 'object')
+        return true;
+
+    if (reference === null || input === null)
+        return input === reference;
+
+    if (Array.isArray(reference)) {
+        // TODO: we dont expect the reference array to hold any
+        //       elements, but we should at maybe check whether
+        //       the input array types are all matching.
+        return true;
+    }
+
+    // we know that `input` is an `object` based on the first
+    // two `if`s, but for some reason typescript doesn't.  :)
+    if (typeof input !== 'object')
+        return false;
+
+    const keys = new Set([
+        ...Object.keys(input),
+        ...Object.keys(reference)
+    ]);
+
+    for (const key of keys) {
+        const _input = input as Record<string, unknown>;
+        const _reference = reference as Record<string, unknown>;
+
+        if (!validateTypes(_input[key], _reference[key])) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+function validateLiteral(value: Optional<string>, allowed: readonly string[]) {
+    return value === undefined || allowed.includes(value);
+}
+
+function validateLiterals(literals: [Optional<string>, readonly string[]][]) {
+    for (const [ value, allowed ] of literals) {
+        if (!validateLiteral(value, allowed))
+            return false;
+    }
+
+    return true;
+}
+
+// performs a basic check on an "untrusted" settings object.
+export function validateSettings(settings: PartialSettings) {
+    if (!settings?.schemaVersion) {
+        return false;
+    }
+
+    return (
+        validateTypes(settings)
+        && validateLiterals([
+            [ settings?.appearance?.theme      , themeOptions ],
+            [ settings?.save?.downloadMode     , downloadModeOptions ],
+            [ settings?.save?.filenameStyle    , filenameStyleOptions ],
+            [ settings?.save?.videoQuality     , videoQualityOptions ],
+            [ settings?.save?.youtubeVideoCodec, youtubeVideoCodecOptions ],
+            [ settings?.save?.savingMethod     , savingMethodOptions ]
+        ])
+    );
+}