ArtikelUtveckling

Regex i AI-kod: hitta ReDoS innan servern fastnar

Ett godkänt test säger vad uttrycket matchar. Det säger inte hur mycket arbete ett avvisat värde kan kräva. Prova kostnaden isolerat och behåll samma valideringsregel när du förenklar.

Publicerad · Senast granskad

Svarta kulor löper genom en maskin med lila, slingrande banor, medan gröna kulor lämnar utloppet till höger.
En nästan godkänd sträng kan ge fler matchningsvägar. Prova kostnaden utanför serverns event-loop.
Rätt svar räcker inte

Regex i AI-kod behöver granskas både för matchningsresultat och för beräkningskostnad. Här använder du ett konstruerat uttryck och små lokala strängar. Exemplet säger inget om hur ofta en kodassistent gör detta fel.

Matchande testfall kan dölja kostnaden

Anta att en kodassistent har skrivit /^(a+)+$/ för ett fält som bara får innehålla gemena ASCII-tecknet a. Testerna med a, aa och aaa blir gröna. Även tom sträng och bokstaven b avvisas som tänkt. Det är ändå för tidigt att godkänna uttrycket för obetrodd indata.

Node beskriver hur en misslyckad matchning kan kräva många försök, trots att en lyckad matchning går fort. Om matchningen körs synkront i serverns event-loop får annat arbete i samma loop vänta. Risken kallas ReDoS, regular expression denial of service: särskild indata gör matchningen oproportionerligt dyr.

Ställ därför två frågor i kodgranskningen: vilka värden ska funktionen godta, och vilket arbete får den lägga på ett avvisat värde? Skriv förväntat booleskt resultat separat från en tidsbudget. Property-baserad testning av AI-genererad kod hjälper dig hitta funktionella gränsfall. Här lägger du till en undersökning av kostnaden.

Varför nästan rätt indata ger fler vägar

I (a+)+ får den inre gruppen ta flera a, samtidigt som hela gruppen får upprepas. Strängen aaaa kan exempelvis fördelas som en grupp med fyra a eller två grupper med två a. Ett avslutande utropstecken gör att matchningen misslyckas efter den upprepade delen. En motor som backtrackar kan då pröva andra fördelningar innan den avvisar strängen.

OWASP tar upp just nästlade upprepningar och uttrycket (a+)+$. Det är en varningssignal att undersöka, inte en fullständig analysmetod för alla uttryck. I vårt prov används 12, 20 och 28 a följt av ett utropstecken. Den längsta av dessa strängar har alltså bara 29 tecken.

Välj motsvarande avvikelse utifrån produktens egen regel: en tillåten början och ett sista tecken som gör värdet ogiltigt. Gissa inte att stora indata behövs. Börja litet i en isolerad körning och bestäm stoppvillkoret innan du ökar längden.

Kör misstänkt matchning i en separat process

Spara koden nedan som regex-lab.mjs i en tom arbetsmapp och kör node regex-lab.mjs. Inga paket behöver installeras. Skriptet skriver regex-result.json bredvid sig. Det är ett lokalt laboratorieprov, utan HTTP-anrop eller server.

De dyrare försöken körs i ett direkt Node-barn med execFile. Enligt dokumentationen för child_process kan moderprocessen skicka vald signal vid timeout. Här sätts 250 millisekunder och SIGKILL. Ingen shell startas och barnet skapar inga egna barn. Moderprocessen inväntar återanropet efter avslutet.

Tidsgränsen är ett stoppvillkor för hela barnkörningen, inklusive processstart. Det är ingen garanti att den observerade väggtiden blir exakt 250 millisekunder. En avslutad körning anger också tiden inne i barnet för själva matchningen. Vid timeout finns ingen färdig matchningstid att rapportera.

Lägg inte en synkron regex och en timer i Promise.race i samma process och kalla det avbrott. Timerns återanrop får vänta när samma event-loop är blockerad. Här ligger kontrollen i en annan process än den dyra matchningen.

import assert from 'node:assert/strict';
import { execFile } from 'node:child_process';
import { writeFileSync } from 'node:fs';

const before = value => /^(a+)+$/.test(value);
const after = value => /^a+$/.test(value);
const MAX_LENGTH = 32;
const validate = (value, match) =>
  typeof value === 'string' && value.length <= MAX_LENGTH && match(value);

// Bara små strängar i semantikprovet; ingen tidsmätning här.
let checked = 0;
const alphabet = ['a', 'b', '!', '\n'];
function visit(value, depth) {
  const expected = value.length > 0 && [...value].every(c => c === 'a');
  assert.equal(before(value), expected);
  assert.equal(after(value), expected);
  checked++;
  if (depth < 6) for (const c of alphabet) visit(value + c, depth + 1);
}
visit('', 0);
for (const value of ['a'.repeat(32), 'a'.repeat(33), 'a\r', 'a\r\n', 'a\u2028', 'a\u2029', 42, null]) {
  const expected = typeof value === 'string' && value.length > 0 &&
    value.length <= MAX_LENGTH && [...value].every(c => c === 'a');
  assert.equal(validate(value, before), expected);
  assert.equal(validate(value, after), expected);
}

const worker = `
  const [mode, countText, suffix] = process.argv.slice(1);
  const pattern = mode === 'before' ? /^(a+)+$/ : /^a+$/;
  const input = 'a'.repeat(Number(countText)) + suffix;
  const start = performance.now();
  const matched = pattern.test(input);
  console.log(JSON.stringify({ matched, regexMs: performance.now() - start }));
`;
function measure(mode, count, suffix) {
  return new Promise((resolve, reject) => {
    const start = performance.now();
    execFile(process.execPath, ['--eval', worker, mode, String(count), suffix], {
      timeout: 250, killSignal: 'SIGKILL', maxBuffer: 4096, windowsHide: true
    }, (error, stdout) => {
      const base = { mode, inputLength: count + suffix.length, wallMs: performance.now() - start };
      if (error) {
        if (error.killed && error.signal === 'SIGKILL') {
          resolve({ ...base, status: 'timeout' });
        } else reject(error);
      } else resolve({ ...base, status: 'completed', ...JSON.parse(stdout) });
    });
  });
}
const rows = [];
for (const count of [12, 20, 28]) {
  for (const mode of ['before', 'after']) rows.push(await measure(mode, count, '!'));
}
rows.push(await measure('before', 28, ''));
// Själva tidsgränsen måste också verifieras. Den här strängen är 33 tecken.
rows.push(await measure('before', 32, '!'));
assert(rows.some(row => row.status === 'timeout'), 'Timeoutvägen måste ha körts');
for (const row of rows) if (row.status === 'completed') {
  assert.equal(row.matched, row.inputLength === 28 && row === rows[rows.length - 2]);
}
const result = { node: process.version, platform: process.platform, timeoutMs: 250,
  semanticStrings: checked, boundaryCases: 8, rows,
  limitation: 'Syntetiskt lokalt prov; processstart ingår i wallMs. Ingen produktionsmätning eller AI-modell har provats.' };
writeFileSync(new URL('./regex-result.json', import.meta.url), JSON.stringify(result, null, 2) + '\n');
console.log(JSON.stringify(result, null, 2));

Jämför mot samma regel och samma längdgräns

Exemplets produktregel är uttryckligen 1–32 gemena ASCII-a. Talet 32 är ett valt exempelkrav, inte en säker gräns för andra regex. Båda versionerna använder samma typkontroll, samma längdspärr och samma anrop till matchningsfunktionen. Skillnaden är uttrycket: /^(a+)+$/ byts mot /^a+$/.

Här kan du även resonera om likvärdigheten: en eller flera icke-tomma grupper som enbart består av a ger samma strängmängd som en enda följd med minst ett a. Inga flaggor används och anroparen läser bara sant eller falskt. Fångstgruppen försvinner i förenklingen; om annan kod använder den måste du bedöma den ändringen separat.

Koden jämför båda uttrycken mot en oberoende förväntan för alla 5 461 strängar upp till sex tecken över alfabetet a, b, utropstecken och radbrytning. Åtta ytterligare fall provar längdgräns, andra radslut och värden som inte är strängar. De korta fallen kompletterar resonemanget; de bevisar inte att en annan regexomskrivning är riktig.

Längdspärren stoppar 33 tecken innan produktens matchningsfunktion anropas. Tidsprovet kör däremot själva uttrycket direkt, även med en syntetisk sträng på 33 tecken, för att verifiera att timeoutvägen fungerar. Dessa två syften ska inte blandas ihop: det ena provar produktbeslutet, det andra isoleringens stoppmekanism.

Vad det utförda provet visar

Den 8 november 2026 kördes provet med lokalt installerad Node.js v24.13.0 på Windows. Båda validerarna gav förväntat resultat i semantikprovet. Före-uttrycket nådde tidsgränsen för 28 a följt av utropstecken, totalt 29 tecken. Efter-uttrycket hann avvisa samma sträng. Före-uttrycket hann också godkänna 28 a utan utropstecken. Det separata timeoutprovet på 33 tecken avbröts.

Resultatet visar skillnaden för dessa strängar i denna miljö. Det mäter inte en produktionsservers kapacitet eller en bestämd hastighetsfaktor mellan uttrycken. När ett barn avbryts vet du att dess hela körning överskred budgeten; du vet inte hur lång tid matchningen skulle ha tagit färdigt.

För en verklig ändring lämnar du regexen, den avsedda regeln, nästan-matchande indata, miljöversionen och körloggen i samma granskningsärende. Koppla gränsen till den kodväg som tar emot värdet. En spärr i ett formulär är inte underlag för att serverns egen validering är avgränsad. Den bredare säkerhetsgranskningen av en AI-byggd kodbas hjälper dig följa värdet genom applikationen.

Statiska regexkontroller kan ge ytterligare ledtrådar men hittar inte alla sårbara uttryck. Ett motorbyte kräver också beteendekontroll; Node varnar för skillnader i kompatibilitet. Godkänn den konkreta förenklingen med dokumenterade förväntningar och behåll reproducerbara fall för nästa ändring.

FAQ

Vad är ReDoS i en regex?

ReDoS är en risk för överbelastning när ett reguljärt uttryck kräver mycket beräkning för särskilt vald indata. I exemplet utlöser ett avslutande utropstecken många möjliga försök i det nästlade uttrycket.

Kan Promise.race stoppa en långsam regex?

En timer i samma event-loop kan inte avbryta en synkron regex som redan körs. I provet utför en separat barnprocess matchningen, och moderprocessen avslutar barnet när tidsgränsen nås.

Räcker en snabb testkörning för att godkänna regexen?

Nej. Provet gäller de valda strängarna och den aktuella miljön. Du behöver också kontrollera den avsedda valideringsregeln, längdgränsen och att en förenkling bevarar beteendet som anroparen använder.

Källor och metod

Källorna kontrollerades 8 november 2026: OWASP om ReDoS för riskmönstret, Node om event-loop och regex för blockerande matchningar och analysens begränsningar samt Node v24.13.0 om barnprocesser för isoleringen. Valideringsregeln och testkoden är artikelns egna exempel. Ingen AI-modell, extern tjänst eller produktionsmiljö ingår i provet.