Skip to content

pam: web app access#151

Closed
gilesv wants to merge 1 commit intoInfisical:mainfrom
gilesv:webapp-access
Closed

pam: web app access#151
gilesv wants to merge 1 commit intoInfisical:mainfrom
gilesv:webapp-access

Conversation

@gilesv
Copy link

@gilesv gilesv commented Mar 16, 2026

Please see: gilesv/infisical#1

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 17, 2026

Greptile Summary

This PR adds web app (web-app) access support to the PAM (Privileged Access Management) proxy, implementing an HTTP/HTTPS reverse proxy handler that intercepts, logs, and forwards browser traffic through the PAM gateway for session auditing. The new WebAppProxy follows the same architecture pattern as the existing Kubernetes proxy (buffered reads, TeeReader-based body capture for logging, keep-alive loop), and is correctly wired into the resource type switch in pam-proxy.go and the session uploader in uploader.go.

Issues found:

  • 12 development session files committed — Encrypted .enc session recording files (with real UUIDs as filenames) are committed to the repository. The session/ directory is not in .gitignore, creating a risk that real session data could be committed accidentally in the future.
  • http.Transport/http.Client created per request — In proxy.go, the transport and client are instantiated on every iteration of the request-handling loop. This disables TCP/TLS connection reuse and connection pooling, causing a new handshake for every proxied request. These should be initialized once per proxy instance.
  • No request body size limitio.ReadAll(req.Body) has no upper bound, making the gateway vulnerable to memory exhaustion if a client sends an oversized body. io.LimitReader should be used.
  • JSON injection in writeErrorResponse — Error messages (including err.Error() output) are interpolated directly into a raw JSON string without escaping, which produces malformed JSON when the message contains quotes or backslashes. encoding/json should be used to marshal the response body.
  • Copy-paste error messages in uploader.go — The HTTP event upload branch (Kubernetes/WebApp) still says "failed to read SSH session file" and "Uploading terminal session events" instead of appropriate HTTP-focused messages.
  • No documentation — No documentation was found for the new web-app resource type. Customers may have difficulty discovering this feature.

Confidence Score: 2/5

  • Not safe to merge — committed session files must be removed, and the DoS and JSON injection issues in the proxy handler should be addressed first.
  • The core logic of the WebApp proxy is sound and follows established patterns in the codebase, but three issues need to be resolved before merging: (1) 12 session files with real UUIDs should not be committed, (2) the lack of a request body size limit is an exploitable DoS vector, and (3) unsanitized error messages produce invalid JSON responses. Additionally, the per-request transport allocation is a notable performance problem. Together these prevent a safe merge.
  • packages/pam/handlers/webapp/proxy.go needs the most attention for the transport-per-request, body size limit, and JSON injection issues. All 12 session/*.enc files must be removed and added to .gitignore.

Important Files Changed

Filename Overview
packages/pam/handlers/webapp/proxy.go New WebApp HTTP proxy handler. Has three notable issues: (1) a new http.Transport/http.Client is created per request instead of being reused, defeating connection pooling; (2) no request body size limit in io.ReadAll which is a DoS vector; (3) raw string interpolation in writeErrorResponse produces malformed JSON when error messages contain special characters.
packages/pam/pam-proxy.go Adds the web-app resource type case to the proxy switch. The integration follows the same pattern as other resource types (Kubernetes, Redis, etc.) and is straightforward. No issues found.
packages/pam/session/uploader.go Adds ResourceTypeWebApp to the filename regex pattern and to the HTTP event upload branch. Correct logic, but contains copy-paste error messages: "failed to read SSH session file" and "Uploading terminal session events" are incorrect for the Kubernetes/WebApp HTTP event path.
session/pam_session_26d12f42-c3e3-4d18-a43e-a2f875adc003_web-app_expires_1773708241.enc One of 12 encrypted session recording files accidentally committed to the repository. These expose session UUIDs and should not be version-controlled. The session/ directory should be added to .gitignore.

Comments Outside Diff (2)

  1. packages/pam/session/uploader.go, line 311-320 (link)

    Copy-paste error in log/error messages

    Two messages in this block are incorrect for the Kubernetes/WebApp HTTP event path — they appear to have been copied from the SSH branch without updating:

    1. Line 313: "failed to read SSH session file" should reference HTTP events, not SSH.
    2. Line 319: "Uploading terminal session events" should be "Uploading HTTP session events" since these are HTTP events, not terminal events.
  2. session/pam_session_26d12f42-c3e3-4d18-a43e-a2f875adc003_web-app_expires_1773708241.enc, line 1 (link)

    Session recording files committed to repository

    12 encrypted session recording files (pam_session_*.enc) have been committed to the repository. These are development/test session files that expose real session UUIDs and shouldn't be version-controlled. Even though the files are currently empty, the filenames themselves expose session UUIDs and this pattern could easily lead to real session data being accidentally committed in the future.

    The session/ directory (or at minimum *.enc files) should be added to .gitignore and these 12 files should be removed from the repository. The same applies to all other similar files in this PR:

    • session/pam_session_39a8bef5-..._web-app_expires_*.enc
    • session/pam_session_3fa66f92-..._web-app_expires_*.enc
    • (and 9 more)

    Add to .gitignore:

    session/
    

Last reviewed commit: 057bdd8

Comment on lines +99 to +106
// Read request body
reqBody, err := io.ReadAll(req.Body)
if err != nil {
l.Error().Err(err).Msg("Failed to read request body")
writeErrorResponse(clientConn, "failed to read request body")
continue
}
req.Body.Close()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No request body size limit (potential DoS)

io.ReadAll(req.Body) reads the entire request body into memory with no upper bound. A malicious or misbehaving client could send an extremely large body (e.g., a multi-GB upload) causing memory exhaustion on the gateway process. Consider wrapping the body with http.MaxBytesReader before reading:

Suggested change
// Read request body
reqBody, err := io.ReadAll(req.Body)
if err != nil {
l.Error().Err(err).Msg("Failed to read request body")
writeErrorResponse(clientConn, "failed to read request body")
continue
}
req.Body.Close()
// Read request body
req.Body = http.MaxBytesReader(nil, req.Body, 32*1024*1024) // 32 MB limit
reqBody, err := io.ReadAll(req.Body)
if err != nil {
l.Error().Err(err).Msg("Failed to read request body")
writeErrorResponse(clientConn, "failed to read request body")
continue
}
req.Body.Close()

Note: http.MaxBytesReader requires an http.ResponseWriter as its first argument; if that doesn't fit here, you can instead use io.LimitReader:

reqBody, err := io.ReadAll(io.LimitReader(req.Body, 32*1024*1024))

Comment on lines +132 to +145
transport := &http.Transport{
DisableKeepAlives: false,
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
TLSClientConfig: p.config.TLSConfig,
}
client := &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
// Don't follow redirects — let the client handle them
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HTTP transport and client created per request

A new http.Transport and http.Client is instantiated on every iteration of the loop, which means:

  1. Connection pooling is effectively disabled — a new TCP/TLS handshake is performed for every proxied request
  2. MaxIdleConns: 10 and DisableKeepAlives: false have no practical effect since the transport is discarded after each request
  3. Under load, this exhausts file descriptors and significantly increases latency

The transport and client should be created once per proxy instance (e.g., in NewWebAppProxy or at the start of HandleConnection) and reused across requests:

// In WebAppProxy struct
type WebAppProxy struct {
    config    WebAppProxyConfig
    transport *http.Transport
    client    *http.Client
}

func NewWebAppProxy(config WebAppProxyConfig) *WebAppProxy {
    transport := &http.Transport{
        DisableKeepAlives: false,
        MaxIdleConns:      10,
        IdleConnTimeout:   30 * time.Second,
        TLSClientConfig:   config.TLSConfig,
    }
    client := &http.Client{
        Transport: transport,
        Timeout:   30 * time.Second,
        CheckRedirect: func(req *http.Request, via []*http.Request) error {
            return http.ErrUseLastResponse
        },
    }
    return &WebAppProxy{config: config, transport: transport, client: client}
}

Comment on lines +194 to +200
func writeErrorResponse(conn net.Conn, message string) {
errResp := fmt.Sprintf(
"HTTP/1.1 502 Bad Gateway\r\nContent-Type: application/json\r\n\r\n{\"message\": \"gateway: %s\"}",
message,
)
conn.Write([]byte(errResp))
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSON injection in error response body

The message argument is interpolated directly into a raw JSON string without escaping. If err.Error() (line 150) returns a string containing double-quotes or backslashes (e.g., a network error mentioning a path like "failed to dial \"target:8080\": ..."), the resulting response body will be malformed JSON.

For example, the call at line 150:

writeErrorResponse(clientConn, fmt.Sprintf("failed to reach target: %s", err.Error()))

could produce: {"message": "gateway: failed to reach target: dial tcp: lookup "evil" ..."} — invalid JSON.

Use encoding/json to safely encode the message:

func writeErrorResponse(conn net.Conn, message string) {
    type errBody struct {
        Message string `json:"message"`
    }
    body, _ := json.Marshal(errBody{Message: "gateway: " + message})
    errResp := fmt.Sprintf(
        "HTTP/1.1 502 Bad Gateway\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n%s",
        len(body), body,
    )
    conn.Write([]byte(errResp))
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant