mediumSeverity: mediumRisk ratingHow usable this gap is to an attacker. Severity sets the posture penalty: critical −40, high −20, medium −10, low −4, info −1. Higher severity means fix it sooner.App server (CORS)Where it livesImplementationThis fix is applied in: App server (CORS). That's the surface you'll edit, DNS, response headers, or a static file.~15 minTime to fixEffortRoughly 15 minutes of hands-on work, propagation aside. Most of these are copy-paste.+10 posture+10 postureScore impactPoints this fix recovers in the ApeCyber posture score (0–100). Posture = 100 minus the severity penalty of every open finding; clearing this finding adds these points back.

CORS: echo one trusted origin, never whatever asks

Check every Origin against a server-side allowlist so only sites you trust can read your API responses

The threat

The scanner sent a request carrying a made-up Origin header (something like https://evil.example.com) and your server echoed that exact value straight back in Access-Control-Allow-Origin. That is the signal: your API reflects whatever origin asks, instead of checking it against a short list of sites you actually trust.

Here is what that means in plain terms. Access-Control-Allow-Origin is the browser’s rule for who is allowed to read your API’s responses. When you reflect any origin, you have effectively told every website on the internet “yes, you may read this.” If credentials are in play too, any site a logged-in visitor happens to open can quietly read their data from your API on their behalf, as if it were them.

The exact fix

The rule is simple: keep a list of origins you trust on the server, and only echo back the request’s Origin when it is on that list. Never reflect an arbitrary origin, and default to same-origin (no CORS header at all) for everything else.

Express (with the cors package). Pass a function, not origin: true (which reflects everything):

const cors = require('cors');

const allowlist = new Set([
  'https://yourdomain.com',
  'https://app.yourdomain.com',
]);

app.use(cors({
  origin(origin, cb) {
    // same-origin and curl (no Origin header) pass through
    if (!origin || allowlist.has(origin)) return cb(null, true);
    return cb(null, false); // not listed: no CORS headers, browser blocks the read
  },
  credentials: true,
}));

Plain Node, or any framework, the same logic by hand:

const allowlist = new Set(['https://yourdomain.com']);
const origin = req.headers.origin;
if (origin && allowlist.has(origin)) {
  res.setHeader('Access-Control-Allow-Origin', origin);
  res.setHeader('Vary', 'Origin'); // caches must key on Origin
  res.setHeader('Access-Control-Allow-Credentials', 'true');
}

nginx, map the trusted origins and only emit the header on a match:

# in the http { } block
map $http_origin $cors_ok {
  default                       "";
  "https://yourdomain.com"      $http_origin;
  "https://app.yourdomain.com"  $http_origin;
}

location /api/ {
  add_header Access-Control-Allow-Origin $cors_ok always;
  add_header Vary Origin always;
}

Gotcha: always send Vary: Origin when the allowed origin depends on the request. Without it a shared cache (a CDN, the browser) can hand one origin’s allowed response to a different origin. And never build the allowlist from user input or an untrusted lookup, keep it hard-coded in config.

Verify it

curl -sI -H "Origin: https://evil.example.com" https://yourdomain.com/api/ | grep -i access-control-allow-origin

A safe result is no Access-Control-Allow-Origin line at all, meaning the stranger origin was refused. Repeat with a real allowlisted origin and you should see exactly that origin echoed back, never * and never the evil one.

Proof

This is one the ApeCyber scanner catches passively on every scan, sending a single throwaway Origin from the outside and watching whether your server reflects it back, touching nothing else. Pin cross-origin reads to an allowlist and that reflection is gone, a clean −10 off your posture score, the same kind of hardening that took dev3lop.com from a failing grade to A / 100.