En trasig CORS-konfiguration är en av de vanligaste säkerhetsbristerna i svenska och nordiska API:er, och den smyger sig in utan att någon märker det. En utvecklare får ett irriterande CORS-fel i webbläsarkonsolen, klistrar in Access-Control-Allow-Origin: * för att “fixa” problemet, och fortsätter koda. Två veckor senare har samma API, med autentiserade cookies påslagna, blivit nåbart från vilken sajt som helst på internet. Enligt OWASP:s API Security Top 10 hör den här typen av misstag hemma under kategorin API8:2023 – Security Misconfiguration, där öppen CORS-policy nämns tillsammans med exponerade adminpaneler och saknade säkerhetshuvuden.

Den här guiden går igenom CORS (Cross-Origin Resource Sharing) från grunden, visar exakt vilka konfigurationer som är farliga, och bygger upp en säker Express-baserad lösning steg för steg på 14 oktober 2026. Du får kodexempel du kan klistra in direkt, en felsökningssektion för de vanligaste CORS-buggarna, och ett komplett projekt att testa lokalt. Målgruppen är backend- och fullstack-utvecklare som bygger API:er för svenska eller nordiska produkter, oavsett om det handlar om en intern mikrotjänst eller ett publikt API som tredjepartsklienter ska kunna anropa.

Vad är CORS och varför finns det?

CORS är en webbläsarmekanism, inte en serverteknik. Den styr vilka ursprung (origins) som får läsa svaret från en begäran som görs från JavaScript i webbläsaren till en annan domän än den sidan är laddad från. Utan CORS hade webbläsarens same-origin policy blockerat all sådan trafik helt, vilket hade gjort moderna API-drivna webbappar praktiskt taget omöjliga att bygga.

Det är här det första och vanligaste missförståndet uppstår. CORS skyddar inte servern, och det är inte autentisering. En angripare som anropar ditt API direkt med curl, ett Python-skript eller ett serverprogram struntar helt i CORS-huvudena, eftersom de bara tolkas av webbläsare. CORS avgör enbart om webbläsaren tillåter JavaScript-koden på en sida att läsa svaret som redan kommit tillbaka från servern. Servern måste alltså fortfarande ha egen autentisering, auktorisering, indatavalidering, CSRF-skydd där det behövs och rate limiting, precis som om CORS inte fanns alls.

Express-projektets officiella dokumentation beskriver cors-middleware som ett sätt att sätta just de HTTP-svarshuvuden som talar om för webbläsaren vilka ursprung som får läsa ett svar. Paketet installeras med npm install cors och fungerar som Connect/Express-middleware. Det är precis den här byggstenen vi kommer konfigurera rätt i den här guiden, i stället för att slentrianmässigt slå på en vid-öppen policy.

Så fungerar Same-Origin Policy i webbläsaren

För att förstå CORS måste man först förstå regeln den gör undantag från, same-origin policy. Den har funnits i webbläsare sedan 1990-talet och säger i grunden att JavaScript som körs på en sida bara får läsa data från samma ursprung som sidan själv kom ifrån. Ett ursprung definieras av tre delar tillsammans: protokoll (http eller https), domän och port. Byt ut vilken som helst av de tre, och webbläsaren räknar det som ett helt annat ursprung, även om domänen i övrigt är identisk.

Det betyder att https://app.example.se och http://app.example.se räknas som två olika origins, trots att de delar domännamn. Samma sak gäller https://app.example.se jämfört med https://api.example.se, subdomäner räknas som skilda ursprung. Den här strikta uppdelningen finns av en enda anledning: utan den skulle vilken sida som helst kunna läsa innehållet från en annan sida en användare råkar vara inloggad på, bara genom att göra ett bakgrundsanrop med JavaScript och läsa svaret.

Problemet är att den strikta regeln också blockerar helt legitim trafik i moderna applikationer, där en frontend ofta ligger på en egen domän eller subdomän skild från backend-API:et. CORS är webbläsartillverkarnas lösning på det dilemmat: i stället för att slopa same-origin policy helt, lät man servern själv, via HTTP-huvuden i sitt svar, uttryckligen tala om vilka andra ursprung den litar på. Webbläsaren respekterar sedan det beskedet och öppnar upp exakt så mycket som servern bett om, inte mer.

Den springande punkten, som många missar, är att detta är ett frivilligt kontrakt som servern själv definierar. En server som svarar med en bred eller felaktig CORS-policy har själv öppnat dörren, webbläsaren gör bara exakt vad den blir tillsagd. Ansvaret för en säker konfiguration ligger alltså helt och hållet hos den som bygger API:et, inte hos webbläsaren eller hos något externt skyddslager.

Förkunskaper och versioner

Innan du börjar behöver du en fungerande Node.js-miljö och grundläggande förståelse för hur ett Express-API byggs upp. Guiden förutsätter att du redan kan starta en enkel server och skicka HTTP-förfrågningar med verktyg som curl eller Postman. Du behöver inte kunna något om CORS sedan tidigare, det är precis det vi bygger upp här.

  • Node.js, senaste LTS-versionen (verifiera med node --version innan du börjar)
  • npm, följer med Node.js-installationen
  • Express som webbramverk
  • cors-paketet (Express-middleware för CORS-huvuden, version 2.8.6 enligt Express officiella dokumentation)
  • En editor, till exempel VS Code
  • curl eller Postman för att testa förfrågningar manuellt
  • Grundläggande kunskap om HTTP-metoder (GET, POST, OPTIONS) och HTTP-huvuden

Verifiera alltid den exakta versionen av cors-paketet i ditt eget projekt med npm list cors innan du går i produktion, eftersom npm-registret uppdateras kontinuerligt och versionsnumret kan ha ändrats sedan den här artikeln skrevs.

Steg 1: Skapa projektstrukturen

Börja med en ren mapp för projektet. Vi bygger ett litet API som representerar ett typiskt scenario, en backend som ska anropas från en separat frontend-app på en annan domän, exakt den situation där CORS blir relevant.

mkdir cors-sakerhet-demo
cd cors-sakerhet-demo
npm init -y
npm install express cors dotenv

Skapa sedan en .env-fil där du kan hålla isär miljöer senare, och en server.js som blir startpunkten. Att separera konfiguration per miljö är avgörande för CORS specifikt, eftersom den tillåtna origin-listan nästan alltid skiljer sig mellan utveckling, staging och produktion.

Steg 2: Bygg en osäker baslinje (för att förstå problemet)

För att förstå varför konfigurationen senare spelar roll är det värt att först se den trasiga versionen som väldigt många projekt faktiskt kör i produktion. Det här är kodmönstret som uppstår när en utvecklare bara vill “få bort CORS-felet” så fort som möjligt.

// server.js — OSÄKER VERSION, använd INTE i produktion
import express from "express";
import cors from "cors";

const app = express();

// Tillåter ALLA origins att läsa svar, oavsett vem som frågar
app.use(cors());

app.get("/api/konto", (req, res) => {
  res.json({ anvandare: "demo", saldo: 14250 });
});

app.listen(3000, () => console.log("Körs på port 3000"));

Anropet cors() utan argument sätter Access-Control-Allow-Origin: * på alla svar. Så länge endpointen inte returnerar personlig eller autentiserad data är det här ofta oskadligt, exempelvis för ett helt publikt, icke-autentiserat API. Problemet uppstår när samma mönster används på endpoints som kräver inloggning eller returnerar känslig information, vilket är precis det OWASP varnar för under Security Misconfiguration-kategorin.

Steg 3: Förstå varför wildcard plus credentials är farligt

Den verkligt farliga kombinationen är wildcard-origin tillsammans med autentiserade förfrågningar (cookies eller Authorization-huvuden). Webbläsarspecifikationen tillåter faktiskt inte denna kombination rakt av, men många utvecklare löser det genom att spegla tillbaka vilket ursprung som helst som skickas in i Origin-huvudet, vilket i praktiken ger samma resultat som en riktig wildcard.

// FORTFARANDE OSÄKERT — speglar tillbaka vilken origin som helst
app.use(cors({
  origin: (origin, callback) => {
    callback(null, true); // accepterar ALLA origins okritiskt
  },
  credentials: true
}));

Den här koden ser ut som att den gör ett aktivt val, men funktionen godkänner i praktiken varje inkommande värde utan kontroll. En angripare kan då bygga en helt vanlig webbsida, be offret besöka den medan offret är inloggad på ditt API i en annan flik, och låta sidans JavaScript göra autentiserade anrop mot ditt API med offrets cookies, och sedan läsa tillbaka svaret. Det är exakt den risk som material kopplat till OWASP:s API-säkerhetsarbete pekar ut, en öppen policy som samtidigt accepterar autentiserade webbläsarförfrågningar.

Steg 4: Bygg en säker allowlist-baserad konfiguration

Nu bygger vi den version som faktiskt ska användas. Grundprincipen är enkel: en explicit, hårdkodad eller konfigurationsstyrd lista över tillåtna origins, aldrig en funktion som accepterar allt den får in.

// server.js — SÄKER VERSION
import express from "express";
import cors from "cors";
import "dotenv/config";

const app = express();

const allowedOrigins = new Set(
  (process.env.ALLOWED_ORIGINS || "").split(",").map(o => o.trim()).filter(Boolean)
);

const corsOptions = {
  origin(origin, callback) {
    // Tillåt server-till-server-anrop utan Origin-huvud (t.ex. curl, interna tjänster)
    if (!origin) return callback(null, true);

    if (allowedOrigins.has(origin)) {
      return callback(null, true);
    }

    return callback(new Error(`Origin ${origin} är inte tillåten av CORS-policyn`));
  },
  methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
  allowedHeaders: ["Content-Type", "Authorization"],
  credentials: true,
  maxAge: 600
};

app.use(cors(corsOptions));

app.get("/api/konto", (req, res) => {
  res.json({ anvandare: "demo", saldo: 14250 });
});

app.listen(3000, () => console.log("Säker server körs på port 3000"));

I din .env-fil anger du sedan exakt vilka ursprung som får anropa API:t:

ALLOWED_ORIGINS=https://app.example.se,https://admin.example.se

Den här lösningen har tre viktiga egenskaper. Dynamisk origin-validering görs mot en kontrollerad lista i stället för att blint spegla inkommande värden. Metoder och headers begränsas till vad klienten faktiskt behöver, i stället för att tillåta allt. Och credentials: true kombineras alltid med en explicit lista, aldrig med wildcard, vilket är precis den gräns webbläsarna själva också tvingar fram på specifikationsnivå.

Steg 5: Förstå preflight-förfrågningar (OPTIONS)

Många CORS-buggar handlar egentligen om preflight-förfrågningar, inte om själva huvudanropet. När en webbläsare ska göra en “icke-enkel” förfrågan, till exempel en PUT eller DELETE, eller en POST med ett eget Content-Type som application/json, skickar den först en separat OPTIONS-förfrågan för att fråga servern om den riktiga förfrågan kommer tillåtas.

Om servern inte svarar korrekt på den OPTIONS-förfrågan, med rätt Access-Control-Allow-Methods, Access-Control-Allow-Headers och samma origin-logik som det riktiga anropet, kommer webbläsaren aldrig ens skicka den faktiska förfrågan. Det är därför ett CORS-fel ofta syns i webbläsarkonsolen trots att servern, testad direkt med curl, verkar fungera perfekt. curl bryr sig inte om preflight, bara webbläsaren gör det.

# Testa en preflight-förfrågan manuellt med curl
curl -i -X OPTIONS http://localhost:3000/api/konto \
  -H "Origin: https://app.example.se" \
  -H "Access-Control-Request-Method: PUT" \
  -H "Access-Control-Request-Headers: Content-Type, Authorization"

Ett korrekt svar ska innehålla statuskod 204 eller 200, samt huvudena Access-Control-Allow-Origin, Access-Control-Allow-Methods och Access-Control-Allow-Headers med värden som matchar det du bad om. maxAge-inställningen i exemplet ovan talar om för webbläsaren hur länge den får cacha preflight-svaret innan den behöver fråga igen, vilket minskar antalet extra request-rundturer.

CORS-huvuden i detalj: vad varje huvud faktiskt gör

Innan du går vidare till hur flera miljöer hanteras är det värt att stanna upp och gå igenom exakt vad varje enskilt CORS-relaterat huvud styr. Många konfigurationsmisstag uppstår för att utvecklare kopierar ett exempel utan att förstå vad de enskilda delarna gör, och råkar därför skala upp ett undantag som egentligen bara skulle gälla en sak.

HuvudSkickas avFunktion
OriginWebbläsaren (begäran)Talar om för servern vilket ursprung förfrågan kommer ifrån
Access-Control-Allow-OriginServern (svar)Anger vilket eller vilka ursprung som får läsa svaret
Access-Control-Allow-CredentialsServern (svar)Tillåter att cookies och autentiseringsuppgifter skickas med i cross-origin-anrop
Access-Control-Allow-MethodsServern (preflight-svar)Listar vilka HTTP-metoder som är tillåtna mot endpointen
Access-Control-Allow-HeadersServern (preflight-svar)Listar vilka anpassade headers klienten får skicka med
Access-Control-Max-AgeServern (preflight-svar)Hur länge webbläsaren får cacha preflight-resultatet, i sekunder
Vary: OriginServern (svar)Säger till cachar att svaret varierar beroende på vilket ursprung som frågade

Ett vanligt nybörjarfel är att anta att Access-Control-Allow-Origin kan sättas till flera domäner separerade med kommatecken i samma svar, exempelvis https://app.example.se, https://admin.example.se. Det fungerar inte. Specifikationen tillåter bara ett enda värde per svar (eller wildcard), vilket är exakt anledningen till att cors-paketets allowlist-mönster, där servern dynamiskt väljer rätt enskilt värde beroende på inkommande Origin, är det korrekta sättet att stödja flera tillåtna domäner samtidigt.

Steg 6: Hantera flera miljöer utan att läcka produktionsdata

Ett klassiskt misstag är att kopiera en bekväm utvecklingskonfiguration, där allt tillåts för att slippa krångel lokalt, rakt in i produktion. Lösningen är att låta origin-listan bero på miljövariabeln och vara strikt striktare ju närmare produktion du kommer.

const ORIGIN_CONFIG = {
  development: ["http://localhost:3000", "http://localhost:5173"],
  staging: ["https://staging.app.example.se"],
  production: ["https://app.example.se", "https://admin.example.se"]
};

const env = process.env.NODE_ENV || "development";
const allowedOrigins = new Set(ORIGIN_CONFIG[env] || []);

if (allowedOrigins.size === 0) {
  throw new Error(`Ingen CORS-origin konfigurerad för miljön "${env}"`);
}

Genom att kasta ett fel om listan är tom tvingar du fram en medveten konfiguration för varje ny miljö i stället för att API:t av misstag startar med en helt öppen eller helt stängd policy. Det är en liten detalj som förhindrar en hel klass av produktionsincidenter orsakade av glömd eller felaktig miljökonfiguration.

Steg 7: Skydda API:er som bara ska anropas server-till-server

Ett ofta förbisett faktum är att CORS i grunden är onödigt för API:er som aldrig ska anropas direkt från JavaScript i en webbläsare. Om ditt API endast pratar med en annan backend-tjänst, en betalningsleverantör eller ett internt batch-jobb, behöver du oftast ingen bred CORS-policy alls, och kan i stället avvisa alla webbläsarursprung som default.

// För rena server-till-server-API:er: neka alla browsers-origins
app.use(cors({
  origin: false // webbläsare får aldrig läsa svaret via CORS
}));

Det här minskar attackytan radikalt, eftersom du helt tar bort möjligheten för skadlig JavaScript-kod på en annan sajt att läsa svar från ditt API, oavsett om användaren råkar vara inloggad eller inte. Kom ihåg att det här inte stoppar direkta anrop utanför webbläsaren, bara webbläsarbaserad cross-origin-läsning, vilket understryker varför CORS måste kombineras med riktig autentisering snarare än ersätta den.

Steg 8: Lägg till indatavalidering och rate limiting som komplement

Eftersom CORS inte är autentisering måste varje endpoint ändå skyddas på egen hand. Ett minimalt men effektivt lager är att kombinera din CORS-policy med rate limiting mot brute-force och grundläggande indatavalidering mot felformaterade eller skadliga payloads.

import rateLimit from "express-rate-limit";

const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minuter
  max: 100, // max 100 förfrågningar per IP under tidsfönstret
  standardHeaders: true,
  legacyHeaders: false
});

app.use("/api", apiLimiter);

Rate limiting i kombination med en strikt CORS-policy gör det betydligt svårare för en angripare att missbruka ditt API även om de hittar ett sätt att kringgå ett av lagren. Försvar i djupled, inte en enda spärr, är vad som faktiskt håller API:er säkra över tid.

Steg 9: Testa konfigurationen med automatiska tester

En CORS-regression är lätt att råka introducera av misstag, till exempel genom att någon av misstag byter ut allowlist-logiken mot en genväg under en snabb bugfix. Skriv därför ett enkelt test som verifierar att okända origins avvisas och att kända origins släpps igenom.

import request from "supertest";
import app from "../server.js";

test("tillåter känd origin", async () => {
  const res = await request(app)
    .get("/api/konto")
    .set("Origin", "https://app.example.se");
  expect(res.headers["access-control-allow-origin"]).toBe("https://app.example.se");
});

test("avvisar okänd origin", async () => {
  const res = await request(app)
    .get("/api/konto")
    .set("Origin", "https://skadlig-sajt.exempel");
  expect(res.headers["access-control-allow-origin"]).toBeUndefined();
});

Det här testet skyddar dig inte bara idag utan fångar upp framtida regressions, exempelvis om någon lägger till en ny deploy-pipeline och råkar återställa standardkonfigurationen till cors() utan argument.

Steg 10: Granska loggar för avvisade origins

När en legitim origin av misstag blockeras är det ofta tyst, webbläsaren visar bara ett generiskt CORS-fel i konsolen utan att servern nödvändigtvis loggar något. Lägg till enkel loggning i callback-funktionen så att du kan se exakt vilka origins som nekas i produktion, och snabbt avgöra om det är ett angreppsförsök eller ett glömt domännamn i allowlisten.

origin(origin, callback) {
  if (!origin) return callback(null, true);

  if (allowedOrigins.has(origin)) {
    return callback(null, true);
  }

  console.warn(`[CORS] Nekad origin: ${origin} vid ${new Date().toISOString()}`);
  return callback(new Error(`Origin ${origin} är inte tillåten av CORS-policyn`));
}

Skicka dessa loggar vidare till ert vanliga övervakningssystem. Ett plötsligt mönster av många olika okända origins mot samma endpoint under kort tid är ofta ett tecken på att någon aktivt provar att kringgå er policy, eller sonderar för att hitta andra brister i API:t.

Steg 11: Kombinera CORS med säkerhetshuvuden

CORS löser ett specifikt problem, men den löser inte alla cross-origin-relaterade risker. Säkerhetshuvuden som Content-Security-Policy, X-Content-Type-Options och Referrer-Policy lägger till ytterligare lager som skyddar mot helt andra attacktyper, till exempel klickkapning och felaktig MIME-sniffning.

import helmet from "helmet";

app.use(helmet());
app.use(cors(corsOptions));

Ordningen spelar mindre roll här än att båda faktiskt finns med, men det är värt att komma ihåg att helmet och cors löser olika problem och att det ena inte kompenserar för att det andra saknas eller är felkonfigurerat.

Steg 12: Driftsätt och verifiera i en riktig miljö

Innan driftsättning, kör en checklista manuellt mot din riktiga domän i stället för att bara lita på lokala tester. Öppna webbläsarens nätverksflik, gör ett riktigt anrop från er frontend mot produktions-API:t, och bekräfta att rätt Access-Control-Allow-Origin-värde kommer tillbaka, att inga wildcard-tecken smugit sig in, och att OPTIONS-förfrågan verkligen går igenom innan huvudanropet skickas.

# Snabb produktionskontroll
curl -i -X GET https://api.example.se/api/konto \
  -H "Origin: https://app.example.se" \
  -H "Authorization: Bearer DITT_TOKEN"

Förvänta dig ett svar där Access-Control-Allow-Origin exakt matchar https://app.example.se, aldrig en asterisk, tillsammans med Access-Control-Allow-Credentials: true om endpointen kräver inloggning.

Vanliga misstag att undvika

  • Wildcard tillsammans med credentials. Att kombinera origin: "*" med credentials: true öppnar upp för att skadlig kod på vilken sajt som helst kan läsa autentiserade svar om webbläsaren tillåter kombinationen i någon konfiguration.
  • Att spegla tillbaka Origin-huvudet okritiskt. En funktion som alltid svarar callback(null, true) oavsett inkommande värde är funktionellt identisk med en wildcard, bara mer dold i koden.
  • Att glömma preflight-hantering. Om OPTIONS-förfrågningar inte besvaras korrekt blockeras hela den riktiga förfrågan av webbläsaren, även om servern egentligen hade godkänt den.
  • Att återanvända en utvecklingskonfiguration i produktion. En bekväm, bred policy som fungerar lokalt ska aldrig deployas rakt av till en miljö med riktiga användare och känslig data.
  • Att tro att CORS är ett säkerhetslager mot direkta API-anrop. CORS stoppar inte curl, skript eller andra servrar, bara webbläsarbaserad JavaScript-läsning av svar.
  • Att sätta en för bred lista med metoder och headers. Att tillåta alla HTTP-metoder och alla headers “för säkerhets skull” ökar bara attackytan i onödan.
  • Att inte logga nekade origins. Utan loggning blir det nästan omöjligt att skilja på ett legitimt konfigurationsfel och ett aktivt angreppsförsök.

Exempel på korrekt output

Så här ser ett korrekt svar ut från en säkert konfigurerad endpoint, hämtat från en förfrågan med en godkänd origin:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.se
Access-Control-Allow-Credentials: true
Vary: Origin
Content-Type: application/json; charset=utf-8

{"anvandare":"demo","saldo":14250}

Lägg märke till Vary: Origin-huvudet. Det talar om för mellanliggande cachar att svaret skiljer sig beroende på vilken origin som frågade, vilket förhindrar att ett cachat svar avsett för en origin av misstag serveras till en annan.

Felsökning: de vanligaste CORS-problemen

SymptomTrolig orsakLösning
“No Access-Control-Allow-Origin header” i konsolenOrigin saknas i allowlisten, eller cors-middleware är inte monterad allsLägg till domänen i allowlisten och verifiera att app.use(cors(...)) körs före dina routes
Fungerar med curl men inte i webbläsarenPreflight-förfrågan (OPTIONS) misslyckas eller besvaras inteTesta OPTIONS manuellt med curl och kontrollera att rätt metoder/headers returneras
Cookies skickas inte med trots credentials: true på klientenServern saknar credentials: true i cors-konfigurationen, eller origin är satt till wildcardSätt credentials: true på servern och säkerställ en exakt origin, aldrig wildcard
Fungerar i utveckling men inte i produktionProduktionsdomänen saknas i den miljöspecifika origin-listanDubbelkolla NODE_ENV och att produktionsdomänen faktiskt finns med i konfigurationen för den miljön
CORS-fel bara på vissa endpointsMiddleware monterad på fel nivå, t.ex. bara på en specifik routerMontera cors globalt på app-nivå, eller se till att varje router som behöver det får middleware
Fel uppstår bara vid PUT/DELETE, inte GETPreflight krävs för dessa metoder men Access-Control-Allow-Methods saknar demLägg till de metoder klienten faktiskt använder i methods-arrayen
Anpassade headers som X-Request-Id blockerasHeadern finns inte med i allowedHeadersLägg till den specifika headern i listan över tillåtna headers
Intermittenta CORS-fel i produktionCachelager eller CDN serverar gamla CORS-huvuden utan Vary: OriginLägg till Vary: Origin och kontrollera cache-regler på CDN/reverse proxy

CORS jämfört med andra skyddsmekanismer

Det är lätt att blanda ihop CORS med andra säkerhetsbegrepp eftersom de alla rör cross-origin-trafik på ett eller annat sätt. Tabellen nedan sammanfattar vad respektive mekanism faktiskt skyddar mot, och vad den inte gör.

MekanismVad den skyddarVad den INTE skyddar
CORSVilka origins webbläsaren tillåter att läsa svar frånDirekta anrop utanför webbläsaren, autentisering, auktorisering
CSRF-tokenAtt en förfrågan verkligen initierats av din egen sidaLäsning av svar via CORS om CORS-policyn är öppen
Autentisering (JWT/sessions)Att användaren faktiskt är den den utger sig för att varaVilka origins som får läsa svaret i webbläsaren
Rate limitingÖverdriven anropsfrekvens från en klient eller IPAtt en obehörig origin läser enstaka svar
Content-Security-PolicyVilka källor en sida själv får ladda skript och resurser frånVilka externa origins som får läsa svar från ditt API

CORS i CI/CD-pipelinen: fånga fel innan driftsättning

De CORS-tester som visades i steg 9 blir mest värdefulla när de körs automatiskt vid varje pull request, inte bara när någon kommer ihåg att köra dem manuellt lokalt. Lägg till dem som ett eget steg i er CI-pipeline, så att ett förslag till kodändring som råkar öppna upp allowlisten eller ta bort origin-kontrollen stoppas innan det ens når en kodgranskare.

# exempel: enkelt CI-steg (GitHub Actions-liknande pseudokod)
name: cors-tester
on: [pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test -- cors

Komplettera gärna med ett statiskt lintningssteg som letar efter riskabla mönster direkt i koden, till exempel förekomster av cors() utan argument, eller en callback-funktion som returnerar callback(null, true) utan föregående villkor. Ett enkelt grep-baserat steg i pipelinen fångar upp de mest uppenbara fallen även innan de automatiska testerna körs, och fungerar som ett billigt första skyddsnät.

Tänk också på kodgranskning som ett eget skyddslager. En ändring som rör filen där CORS-konfigurationen bor förtjänar extra uppmärksamhet vid granskning, på samma sätt som ändringar i autentiseringslogik eller behörighetskontroller normalt redan får. Många team inför en regel om att just dessa filer alltid kräver godkännande från minst en senior utvecklare innan de kan mergas.

CORS och dataskydd för svenska och nordiska API:er

För bolag som hanterar personuppgifter, vilket i princip gäller alla API:er med inloggade användare, är en trasig CORS-konfiguration inte bara en teknisk brist utan också en potentiell efterlevnadsfråga. Om en felkonfigurerad policy gör det möjligt för en skadlig sida att läsa autentiserade svar som innehåller personuppgifter, kan det i värsta fall klassas som en personuppgiftsincident som ska bedömas enligt gällande dataskyddsregler, med allt vad det innebär av dokumentation och eventuell anmälan.

Det gör CORS-granskning till en naturlig del av den säkerhetsgenomgång många svenska och nordiska bolag redan är skyldiga att göra inom ramen för sitt systematiska informationssäkerhetsarbete. Att inkludera en specifik kontrollpunkt för CORS-konfiguration i den årliga säkerhetsgranskningen, eller i samband med varje större API-lansering, är ett enkelt sätt att fånga upp den här typen av brister innan de blir ett verkligt problem snarare än en teoretisk risk.

Praktiskt innebär det att CORS-konfigurationen bör vara en del av den dokumentation som finns tillgänglig vid en eventuell revision eller incidentutredning: vilka origins som är tillåtna, varför just de valts, och vem som senast granskat och godkänt listan. Den typen av spårbarhet är ofta billig att bygga upp i förväg, men mycket dyrare att rekonstruera i efterhand om något väl går fel.

Avancerade tips för API:er med flera klienttyper

Större API:er har ofta flera klienttyper samtidigt, en officiell webbapp, en mobilapp och kanske tredjepartsintegrationer. Mobilappar och server-till-server-klienter skickar normalt inget Origin-huvud alls, vilket innebär att din callback-funktion måste hantera origin === undefined medvetet snarare än att automatiskt släppa igenom eller blockera det.

Om du behöver öppna upp för externa partners utan att ge dem full tillgång till din interna frontend-konfiguration, överväg en separat, mer begränsad CORS-policy på en dedikerad del av API:t, exempelvis under /public-api, i stället för att bredda huvud-API:ets allowlist för att passa ett enda specialfall. Det håller din kärnkonfiguration enkel att granska och minskar risken att ett undantag för en partner råkar öppna upp hela API:t.

För API:er som körs bakom flera subdomäner, undvik att lösa det genom att tillåta ett helt domänmönster med reguljära uttryck utan eftertanke. En regex som /\.example\.se$/ kan se säker ut men råkar även matcha skadlig-sajt-example.se om den inte är strikt förankrad. Testa alltid regex-baserade origin-kontroller mot både giltiga och ogiltiga exempel innan de går i produktion.

CORS bakom CDN, API-gateway och reverse proxy

I verkliga driftsmiljöer ligger ett Express-API sällan direkt exponerat mot internet. Oftast sitter det bakom en reverse proxy som Nginx, en molnbaserad API-gateway, eller ett CDN-lager för caching och DDoS-skydd. Varje sådant mellanlager kan antingen lägga till egna CORS-huvuden, ta bort de som Express redan satt, eller cacha svar på ett sätt som bryter mot din avsedda policy om det inte konfigureras rätt.

Ett vanligt scenario är att ett CDN cachar ett API-svar inklusive dess CORS-huvuden, och sedan serverar exakt samma cachade svar till en annan origin som frågar strax efter. Utan ett korrekt Vary: Origin-huvud kan det innebära att origin A:s tillåtna svar råkar serveras till origin B, vilket i värsta fall läcker data mellan olika klienter av misstag. Det är därför Vary: Origin inte är en kosmetisk detalj, utan en förutsättning för att CORS och cachning ska kunna samexistera säkert.

Om du använder en molnbaserad API-gateway, exempelvis AWS API Gateway eller en motsvarande tjänst hos Azure eller GCP, har de ofta ett eget CORS-lager som konfigureras separat från din applikationskod. Det är lätt att hamna i en situation där både gatewayen och din Express-app försöker sätta CORS-huvuden samtidigt, vilket kan resultera i dubblerade eller motstridiga värden som webbläsaren i bästa fall ignorerar och i sämsta fall blockerar helt. Bestäm därför medvetet var i kedjan CORS-policyn faktiskt ska hanteras, och stäng av den på övriga lager för att undvika kollisioner.

Testa alltid den fullständiga kedjan, inte bara applikationen isolerat. Ett CORS-svar som ser korrekt ut när du anropar Express-servern direkt på localhost kan se helt annorlunda ut när samma förfrågan passerar genom produktionens reverse proxy, load balancer och eventuella CDN innan den når webbläsaren.

Komplett fungerande projekt

Nedan är hela, körbara projektet samlat på ett ställe. Spara filerna enligt strukturen och kör npm install följt av node server.js för att testa lokalt.

// package.json
{
  "name": "cors-sakerhet-demo",
  "version": "1.0.0",
  "type": "module",
  "scripts": { "start": "node server.js", "test": "jest" },
  "dependencies": {
    "express": "^4.19.2",
    "cors": "^2.8.6",
    "dotenv": "^16.4.5",
    "helmet": "^7.1.0",
    "express-rate-limit": "^7.2.0"
  }
}
// server.js — komplett, säker version
import express from "express";
import cors from "cors";
import helmet from "helmet";
import rateLimit from "express-rate-limit";
import "dotenv/config";

const app = express();
app.use(express.json());
app.use(helmet());

const ORIGIN_CONFIG = {
  development: ["http://localhost:5173"],
  production: (process.env.ALLOWED_ORIGINS || "").split(",").filter(Boolean)
};

const env = process.env.NODE_ENV || "development";
const allowedOrigins = new Set(ORIGIN_CONFIG[env] || []);

const corsOptions = {
  origin(origin, callback) {
    if (!origin) return callback(null, true);
    if (allowedOrigins.has(origin)) return callback(null, true);
    console.warn(`[CORS] Nekad origin: ${origin}`);
    return callback(new Error(`Origin ${origin} nekad av CORS-policy`));
  },
  methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
  allowedHeaders: ["Content-Type", "Authorization"],
  credentials: true,
  maxAge: 600
};

app.use(cors(corsOptions));

const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100 });
app.use("/api", apiLimiter);

app.get("/api/konto", (req, res) => {
  res.json({ anvandare: "demo", saldo: 14250 });
});

app.use((err, req, res, next) => {
  if (err.message?.includes("CORS")) {
    return res.status(403).json({ fel: "Origin inte tillåten" });
  }
  next(err);
});

app.listen(3000, () => console.log(`Server körs i miljö: ${env}`));

Testa sedan med curl enligt exemplen tidigare i artikeln, både med en godkänd och en icke godkänd origin, för att se skillnaden i svar.

Så undviker du att “lösa” CORS-fel genom att stänga av säkerheten

Ett mönster som dyker upp om och om igen när ett team stressar inför en deadline är att CORS-felet “löses” genom att helt enkelt ta bort kontrollen i stället för att fixa den bakomliggande orsaken. Det kan handla om att byta ut en fungerande men strikt allowlist mot cors() utan argument “bara för att komma vidare”, med en mental notering om att “fixa det ordentligt senare”. Den notisen glöms nästan alltid bort, och den tillfälliga lösningen blir permanent produktionskod.

Ett bättre mönster när du stöter på ett CORS-fel mitt i en pressad situation är att först identifiera exakt vilken origin som faktiskt blockeras, och sedan lägga till just den i allowlisten, i stället för att ta bort hela kontrollen. Felmeddelandet i webbläsarkonsolen anger nästan alltid vilket ursprung som nekades, vilket gör den riktade fixen lika snabb att implementera som genvägen, men utan att öppna upp API:t i onödan.

Om teamet upprepade gånger frestas att ta genvägen är det ofta ett tecken på att processen för att lägga till nya tillåtna origins är för tung eller för långsam, snarare än att CORS i sig är problemet. Att korta ner den processen, exempelvis genom att låta allowlisten styras av en miljövariabel som kan uppdateras utan en hel kodgranskning och ny driftsättning, löser det underliggande problemet utan att kompromissa med säkerheten.

Checklista inför driftsättning

  • Ingen wildcard (*) i Access-Control-Allow-Origin på endpoints som kräver inloggning
  • Ingen funktion som okritiskt speglar tillbaka inkommande Origin-värden
  • Explicit allowlist per miljö, med fel som kastas om listan är tom i produktion
  • Preflight-förfrågningar testade manuellt för varje HTTP-metod som används
  • Loggning av nekade origins kopplad till övervakningssystemet
  • Rate limiting och indatavalidering aktiverade som komplement, inte som ersättning för CORS
  • Vary: Origin satt för att undvika felaktig cachning bakom CDN eller reverse proxy
  • Automatiska tester som verifierar både godkända och nekade origins

Vanliga frågor om CORS-säkerhet

Räcker CORS som enda skydd för ett API?
Nej. CORS styr bara vad webbläsare får läsa, inte vem som får anropa API:t. Autentisering, auktorisering och indatavalidering behövs alltid utöver CORS.

Kan jag använda wildcard-origin om mitt API inte kräver inloggning?
Det är möjligt för helt publik, icke-känslig data, men riskbedöm alltid om API:t kan komma att returnera känslig information i framtiden innan du låser in den öppna policyn.

Varför fungerar mitt API i Postman men inte i webbläsaren?
Postman och curl skickar inte webbläsarens same-origin policy eller preflight-logik, så CORS-huvuden testas inte alls på samma sätt som i en riktig webbläsare.

Måste jag hantera OPTIONS-metoden manuellt?
Med cors-paketet i Express hanteras preflight automatiskt så länge middleware är korrekt monterad före dina routes, du behöver normalt inte skriva egen OPTIONS-hantering.

Hur testar jag CORS-konfigurationen snabbt utan att skriva kod?
Använd curl med -H "Origin: ..." mot både en godkänd och en ogiltig domän och jämför svarens huvuden, eller öppna webbläsarens nätverksflik och granska det faktiska anropet.

Ska jag tillåta alla subdomäner med en wildcard i domänen?
Undvik lösa reguljära uttryck för subdomänmatchning. Lista i stället varje tillåten subdomän explicit, eller använd ett strikt förankrat mönster som testats mot både giltiga och skadliga exempel.

Påverkar CORS mobilappar?
Nej, CORS gäller webbläsarkontext. Native mobilappar skickar normalt inget Origin-huvud och träffas inte av CORS-kontrollen på samma sätt, men servern bör ändå kräva egen autentisering för dessa anrop.

Var hör CORS-felkonfiguration hemma i OWASP:s API Security Top 10?
Under kategorin API8:2023, Security Misconfiguration, tillsammans med andra konfigurationsfel som exponerade adminytor och saknade säkerhetshuvuden.