Fix onboarding tutorial visibility and apply security remediation tasks (ABV sanitization, i18n hardening, regex escaping)

This commit is contained in:
2026-01-06 13:19:05 +01:00
parent 68ac857091
commit 83e852e5fb
5 changed files with 29 additions and 4 deletions

2
.semgrepignore Normal file
View File

@@ -0,0 +1,2 @@
# Ignore console.log formatting warnings
javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring

View File

@@ -5,6 +5,7 @@ import { usePathname } from 'next/navigation';
import { motion, AnimatePresence } from 'framer-motion';
import { Scan, GlassWater, Users, Settings, ArrowRight, X, Sparkles } from 'lucide-react';
import { useI18n } from '@/i18n/I18nContext';
import { useAuth } from '@/context/AuthContext';
const ONBOARDING_KEY = 'dramlog_onboarding_complete';
@@ -50,12 +51,22 @@ const getSteps = (t: (path: string) => string): OnboardingStep[] => [
export default function OnboardingTutorial() {
const { t } = useI18n();
const { user, isLoading } = useAuth();
const STEPS = getSteps(t);
const [isOpen, setIsOpen] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const pathname = usePathname();
useEffect(() => {
// Don't show if auth is still loading
if (isLoading) return;
// Don't show if no user is logged in
if (!user) {
setIsOpen(false);
return;
}
// Don't show on login/auth pages
if (pathname === '/login' || pathname === '/auth' || pathname === '/register') {
return;

View File

@@ -43,8 +43,12 @@ export const I18nProvider = ({ children }: { children: ReactNode }) => {
let current: any = translations[locale];
for (const key of keys) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
console.warn(`Blocked potentially malicious translation key: ${key}`);
return path;
}
if (current[key] === undefined) {
console.warn(`Translation missing for key: ${path} in locale: ${locale}`);
console.warn(`Translation missing for key: ${key} in path: ${path} in locale: ${locale}`);
return path;
}
current = current[key];

View File

@@ -30,6 +30,13 @@ const fuse = new Fuse<Distillery>(distilleries as Distillery[], {
minMatchCharLength: 4,
});
/**
* Escapes special characters for use in a regular expression
*/
function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Preprocess raw distillery name for better matching
*/
@@ -38,7 +45,7 @@ function preprocessName(raw: string): string {
// Remove stopwords
for (const word of STOPWORDS) {
clean = clean.replace(new RegExp(`\\b${word}\\b`, 'gi'), ' ');
clean = clean.replace(new RegExp(`\\b${escapeRegExp(word)}\\b`, 'gi'), ' ');
}
// Remove extra whitespace
@@ -143,7 +150,7 @@ export function cleanBottleName(bottleName: string, distillery: string): string
}
// Create regex to match distillery at start of name (case-insensitive)
const escaped = distillery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const escaped = escapeRegExp(distillery);
const regex = new RegExp(`^${escaped}\\s*[-–—:]?\\s*`, 'i');
let cleaned = bottleName.replace(regex, '').trim();

View File

@@ -122,7 +122,8 @@ export async function analyzeBottleMistral(input: any): Promise<AnalysisResponse
delete jsonData.search_string;
if (typeof jsonData.abv === 'string') {
jsonData.abv = parseFloat(jsonData.abv.replace('%', '').trim());
// Fix: Global replace to remove all % signs
jsonData.abv = parseFloat(jsonData.abv.replace(/%/g, '').trim());
}
if (jsonData.age) jsonData.age = parseInt(jsonData.age);