Back to blog
May 10, 2026

Go Security Audit: Finding CORS Reflection, Auth Bypasses, and Error Leaks in an A2A Agent

GoSecurityCORSAuthA2AVueFirebase

After migrating from Node.js to Go, security vulnerabilities don't just disappear — they take new forms. Here's what we found in the new A2A agent and Vue frontend.

Go Security Audit: Finding CORS Reflection, Auth Bypasses, and Error Leaks in an A2A Agent

The Setup

We recently migrated our A2A agent from a Node.js/Express service to a Go-based one using the Google ADK framework. The old agent had a documented set of security vulnerabilities that we fixed in a previous audit (see Security Audit: Finding Auth Bypasses in an A2A Protocol App). The Go rewrite was supposed to be "cleaner and more secure."

We were wrong.

A new codebase doesn't mean new vulnerabilities — it means different vulnerabilities. The architecture had changed enough that old bugs were gone, but new ones had crept in. This post covers the five issues we found in the Go agent and the Vue frontend, and how we fixed each one.

Where We Looked

A castle with all gates open and arrows flying in from every direction — representing unrestricted CORS access

We audited four layers of the stack:

1. Transport — CORS policy (what origins can call the API?)

2. Authentication — Firebase Auth middleware (can requests slip through?)

3. API Responses — Error messages (what do we leak to callers?)

4. Frontend — CSP headers and debug logging (what does the browser expose?)

Each layer revealed something worth fixing.

1. CORS: The Reflecting Shield

The Problem

The CORS middleware in our Go server had a very convenient — and very dangerous — design:

origin := r.Header.Get("Origin")
if origin != "" {
    w.Header().Set("Access-Control-Allow-Origin", origin)
}

This echoes back whatever Origin header the client sends. If a website at https://evil.com makes a request, the server responds with Access-Control-Allow-Origin: https://evil.com. The browser sees a matching origin and allows the cross-origin request.

During development, this is undeniably useful. Your Vite dev server at localhost:5173 works without any configuration. But in production, it means any website can make authenticated requests to your API from a victim's browser.

We also had a fallback for requests without an Origin header:

w.Header().Set("Access-Control-Allow-Origin", "*")

This was equally permissive. And combined with our Cloud Run deployment being --allow-unauthenticated, there was zero protection at the infrastructure level.

The Fix

We replaced the reflect-everything approach with a configured allowlist:

allowedOrigins := config.Load().AllowedOrigins
if origin != "" {
    if isOriginAllowed(origin, allowedOrigins) {
        w.Header().Set("Access-Control-Allow-Origin", origin)
    }
}

The allowed origins come from a CORS_ALLOWED_ORIGINS environment variable — a comma-separated list. For development it's http://localhost:5173,http://localhost:8080. For production it's the Firebase Hosting domains:

CORS_ALLOWED_ORIGINS: https://XXX.firebaseapp.com,https://XXX.web.app

No more wildcard. No more reflection. If an origin isn't in the list, the browser blocks the cross-origin read.

2. Auth: The Silent Backdoor

The Problem

The Firebase Auth middleware had a design flaw that wasn't obvious until you followed the code path:

func getFirebaseAuth() *auth.Client {
    authOnce.Do(initFirebaseAuth)
    return firebaseAuth
}

initFirebaseAuth checks if FIRESTORE_PROJECT_ID is set. If it's empty (or if Firebase initialization fails), getFirebaseAuth() returns nil.

Now here's where it gets interesting. The middleware handles this:

if getFirebaseAuth() == nil {
    ctx := context.WithValue(r.Context(), ContextUserIDKey, "user")
    next.ServeHTTP(w, r.WithContext(ctx))
    return
}

If Firebase isn't configured, every request is allowed through with user ID "user". All requests get grouped under a single anonymous user. There's no authentication check at all.

Diagram showing an unauthenticated request bypassing the auth middleware through the nil-fallback path

This isn't just a development convenience — it's a production vulnerability. If the server starts but Firebase initialization fails (network hiccup, misconfigured env vars, credential issue), the service silently becomes open to everyone.

The Fix

We removed the nil-fallback entirely. If Firebase Auth isn't available, the server returns 401:

if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
    writeJSONError(w, http.StatusUnauthorized, "missing or invalid authorization header")
    return
}

For local development, we added an explicit SKIP_AUTH=true env var. You have to knowingly opt out of auth — it doesn't happen by accident.

The Lesson

"Fail open" patterns in auth middleware are dangerous. Always fail closed: if you can't verify who someone is, reject them. The inconvenience of a 401 during a misconfiguration is far better than silently exposing all your data.

3. Error Messages: The Leaky Faucet

The Problem

This one was subtle but pervasive. Our API handlers used fmt.Sprintf to build error messages that included Go-level error details:

writeJSONError(w, http.StatusBadRequest,
    fmt.Sprintf("invalid request body: %v", err))

writeJSONError(w, http.StatusInternalServerError,
    fmt.Sprintf("failed to fetch story: %v", err))

writeJSONError(w, http.StatusInternalServerError,
    fmt.Sprintf("pdf generation failed: %v", err))
Server console with error messages leaking through cracks

Each of these reveals different internal details to the API consumer:

  • JSON parsing errors expose the structure of the request body
  • Firestore errors expose database collection names and document IDs
  • PDF library errors expose file paths and internal state
  • An attacker probing the API gets a detailed map of our internal architecture — what libraries we use, what our data model looks like, what our storage paths are.

    The Fix

    We now log the full error server-side and return a static message to the client:

    log.Printf("pdf generate: invalid request body: %v", err)
    writeJSONError(w, http.StatusBadRequest, "invalid request body")

    The client gets "invalid request body" — enough to know something went wrong, not enough to map the internals. The full error goes to stdout where we can debug it.

    The Rule

    Error messages should tell the client _what_ went wrong, not _how_ the system works internally. Log the details, hide the internals.

    4. Frontend: Console Spam and No CSP

    The Problem

    The Vue frontend had two issues that compounded each other:

    First, our auth store logged user UIDs to the browser console:

    console.log("[Auth] store: user authenticated —", firebaseUser.uid);
    console.log("[Auth] store: auth state changed —", firebaseUser?.uid ?? "null");

    Seven console.log calls across three files, all logging Firebase user UIDs and auth state transitions. Anyone with browser DevTools open can see authenticated user identifiers.

    Second, there was no Content-Security-Policy header:

    Cross-Origin-Opener-Policy: unsafe-none
    Cross-Origin-Embedder-Policy: unsafe-none
    A web browser shield with cracks representing missing CSP

    The only security headers were the permissive COOP/COEP defaults from Vite. No CSP means:

  • Injected scripts can call out to any server
  • The app can be embedded in an iframe on another site (clickjacking)
  • Mixed content warnings are possible
  • No protection against data injection attacks
  • The Fix

    We removed all console.log calls from the auth flow:

    src/firebase/auth.ts      — 7 logs removed
    src/stores/auth.store.ts  — 10 logs removed (kept the timeout warning)
    src/App.vue               — 3 logs removed

    And added a strict CSP to firebase.json:

    {
      "key": "Content-Security-Policy",
      "value": "default-src 'self'; script-src 'self' https://*.firebaseio.com https://*.googleapis.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https://storage.googleapis.com https://*.googleapis.com; connect-src 'self' https://backend-xxxx.run.app https://*.googleapis.com font-src 'self' https://fonts.gstatic.com; frame-ancestors 'none'"
    }

    The CSP restricts:

  • Scripts to 'self', Firebase, and Google APIs only
  • Images to 'self', GCS, and Google APIs
  • API calls to our fable-go backend URL and Firebase services
  • Frames — disabled entirely with frame-ancestors 'none'
  • The Pattern

    Frontend security isn't just about sanitizing user input. CSP headers and avoiding debug logging are basic hygiene that often get overlooked in the rush to ship features.

    5. Content Validation: The List of Nothing

    The Problem

    The Go agent had a content validation feature. It checked story titles and content for profanity before saving. The ValidateContent function called checkProfanity which iterated over...

    ...an empty slice:

    var profanityList = []string{}

    The function existed, the pipeline called it, the logic was all there — but the list had zero words. The profanity check was a no-op that always passed.

    The Fix

    We populated the list and fixed the matching to use word boundaries:

    var profanityList = []string{
        "fuck", "shit", "asshole", "bastard", "bitch",
        "damn", "crap", "dick", "piss", "slut",
        "whore", "cock", "porn", "sex", "sexy",
    }

    The original implementation used strings.Contains which would match substrings — so "classic" would match "ass". We switched to regex word-boundary matching:

    pattern := regexp.MustCompile(`(?i)\b` + regexp.QuoteMeta(word) + `\b`)

    Now "classic" passes, but "ass" in isolation does not.

    The Lesson

    A validation feature that validates nothing is worse than having no validation at all — it creates a false sense of security.

    Results

    #
    Issue
    Severity
    What We Fixed
    ---
    --------------------------------
    ----------
    ----------------------------------------------
    1
    CORS reflecting any Origin
    **High**
    Replaced with `CORS_ALLOWED_ORIGINS` allowlist
    2
    Auth nil-fallback bypass
    **High**
    Removed fallback, added `SKIP_AUTH` dev flag
    3
    Error messages leaking internals
    **Medium**
    Static messages to clients, full errors logged
    4
    No CSP + debug log leakage
    **Medium**
    Added strict CSP, removed console.log(UUIDs)
    5
    Empty profanity list
    **Low**
    Populated list with word-boundary matching

    Takeaways

    1. A rewrite doesn't reset security.

    Moving from Node.js to Go fixed the old bugs but introduced new ones. Each new codebase has its own attack surface. Audit early, audit often.

    2. "Fail closed" is the only safe default.

    If your auth middleware can't verify someone's identity, reject them. A 401 during a configuration hiccup is far better than a week of data exposure before you notice.

    3. Error messages are a data leak.

    Every %v in an error response is a potential information disclosure. Log the details, return the summary.

    4. CSP isn't optional.

    A few lines in firebase.json prevent XSS, clickjacking, and data injection. There's no reason not to have it.

    5. Empty validation is worse than no validation.

    If you build a profanity checker but populate it with an empty list, you've created a false sense of security for zero benefit.


    Check more open source repos at: github.com/mnkrana/