Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,93 @@ You can use the embedded live view to monitor and control the browser. The live
- Audio streaming in the WebRTC implementation is currently non-functional and needs to be fixed.
- The live view is read/write by default. You can set it to read-only by adding `-e ENABLE_READONLY_VIEW=true \` in `docker run`.

## Proxy Configuration

The headful image supports configuring an HTTP/SOCKS proxy for the browser using a Chrome extension. This is useful for:
- Using residential/datacenter proxies (e.g., Bright Data, Oxylabs)
- Geo-targeting specific locations
- Rotating IP addresses

### Quick Start

1. **Start the container:**
```bash
cd images/chromium-headful
IMAGE=kernel-docker ENABLE_WEBRTC=true ./run-docker.sh
```

2. **Upload the proxy extension:**
```bash
# Create zip from the extension folder (files must be at root level, not in a subdirectory)
cd brightdata-proxy-extension && zip -r ../proxy-extension.zip . && cd ..

# Upload via API
curl -X POST "http://localhost:444/chromium/upload-extensions-and-restart" \
-F "extensions.zip_file=@proxy-extension.zip;filename=ext.zip" \
-F "extensions.name=proxy-extension"
```

3. **Configure the proxy:**
```bash
curl -X PUT http://localhost:444/proxy/config \
-H "Content-Type: application/json" \
-d '{
"host": "brd.superproxy.io",
"port": 33335,
"username": "brd-customer-XXXXX-zone-XXXXX",
"password": "your-password",
"scheme": "http"
}'
```

4. **Restart Chromium to apply:**
```bash
docker exec <container-name> supervisorctl restart chromium
```

### Geo-targeting

Append the country code to the username for geo-targeting:

| Country | Username Suffix |
|---------|-----------------|
| India | `-country-in` |
| United States | `-country-us` |
| United Kingdom | `-country-gb` |
Comment on lines +180 to +184
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix table divider spacing to satisfy markdownlint MD060.

✅ Proposed formatting fix
-|---------|-----------------|
+| ------- | ---------------- |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| Country | Username Suffix |
|---------|-----------------|
| India | `-country-in` |
| United States | `-country-us` |
| United Kingdom | `-country-gb` |
| Country | Username Suffix |
| ------- | ---------------- |
| India | `-country-in` |
| United States | `-country-us` |
| United Kingdom | `-country-gb` |
🧰 Tools
🪛 markdownlint-cli2 (0.20.0)

181-181: Table column style
Table pipe is missing space to the right for style "compact"

(MD060, table-column-style)


181-181: Table column style
Table pipe is missing space to the left for style "compact"

(MD060, table-column-style)


181-181: Table column style
Table pipe is missing space to the right for style "compact"

(MD060, table-column-style)


181-181: Table column style
Table pipe is missing space to the left for style "compact"

(MD060, table-column-style)

🤖 Prompt for AI Agents
In `@README.md` around lines 180 - 184, The Markdown table header and divider need
consistent spacing so markdownlint MD060 is satisfied: edit the table that
starts with the header "Country | Username Suffix" and the rows containing
`-country-in`, `-country-us`, and `-country-gb` to use uniform single spaces
around each pipe and ensure the separator row uses matching column widths (e.g.,
`|---------|-----------------|`) so the pipes in the header, separator, and data
rows align consistently.

| Germany | `-country-de` |

Example:
```json
"username": "brd-customer-XXXXX-zone-XXXXX-country-in"
```

### API Reference

```bash
# Set proxy configuration
curl -X PUT http://localhost:444/proxy/config \
-H "Content-Type: application/json" \
-d '{
"host": "brd.superproxy.io",
"port": 33335,
"username": "brd-customer-XXXXX-zone-XXXXX",
"password": "your-password",
"scheme": "http"
}'

# Get current proxy configuration
curl http://localhost:444/proxy/config

# Disable proxy
curl -X DELETE http://localhost:444/proxy/config
```

### Notes
- Port `444` maps to the kernel-images API (internal port `10001`)
- After updating proxy config, restart Chromium for the extension to apply changes
- Supported schemes: `http`, `https`, `socks4`, `socks5`
- Default bypass list: `["localhost", "127.0.0.1"]`

## Replay Capture

You can use the embedded recording server to capture recordings of the entire screen in our headful images. It allows for one recording at a time and can be enabled with `WITH_KERNEL_IMAGES_API=true`
Expand Down
78 changes: 78 additions & 0 deletions brightdata-proxy-extension/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Bright Data Proxy Extension - API Configured
const CONFIG_URL = "http://127.0.0.1:10001/proxy/config";

let proxyConfig = null;

// Fetch proxy configuration from API
async function fetchProxyConfig() {
try {
const response = await fetch(CONFIG_URL);
if (!response.ok) {
console.error("Failed to fetch proxy config:", response.status);
return null;
}
return await response.json();
} catch (error) {
console.error("Error fetching proxy config:", error);
return null;
}
}
Comment on lines +20 to +33
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid clearing proxy settings on transient API failures.

Any fetch error currently returns null, which triggers applyProxySettings(null) and clears the proxy. This can drop proxying during temporary API outages. Distinguish “no config” from “fetch failed,” and skip apply on errors.

🛠️ Suggested fix
 async function fetchProxyConfig() {
   try {
     const response = await fetch(CONFIG_URL);
-    if (!response.ok) {
-      console.error("Failed to fetch proxy config:", response.status);
-      return null;
-    }
+    if (response.status === 404 || response.status === 204) {
+      return null; // explicitly disabled
+    }
+    if (!response.ok) {
+      console.error("Failed to fetch proxy config:", response.status);
+      return undefined;
+    }
     return await response.json();
   } catch (error) {
     console.error("Error fetching proxy config:", error);
-    return null;
+    return undefined;
   }
 }
 
 async function checkAndApplyConfig() {
   const config = await fetchProxyConfig();
+  if (config === undefined) return;
   const newHash = hashConfig(config);
   const stored = await chrome.storage.local.get(["lastConfigHash"]);

Also applies to: 76-85

🤖 Prompt for AI Agents
In `@brightdata-proxy-extension/background.js` around lines 20 - 33,
fetchProxyConfig currently returns null for both "no config" and fetch errors,
causing applyProxySettings(null) to clear proxy on transient failures; change
fetchProxyConfig to distinguish these cases: when response.ok is false, if
response.status indicates "no config" (e.g. 404) return null, but for other
non-ok statuses and for caught network errors return a distinct failure value
(e.g. undefined) and log the error; then update the caller(s) that call
applyProxySettings to only call it when the returned value is not undefined
(i.e. only apply when config !== undefined), so transient fetch failures skip
applying/clearing settings.


// Apply proxy settings
function applyProxySettings(config) {
if (!config || !config.host || !config.port) {
console.log("No valid proxy config, disabling proxy");
chrome.proxy.settings.clear({ scope: "regular" });
return;
}

proxyConfig = config;

chrome.proxy.settings.set({
value: {
mode: "fixed_servers",
rules: {
singleProxy: {
scheme: config.scheme || "http",
host: config.host,
port: config.port
},
bypassList: config.bypassList || ["localhost", "127.0.0.1"]
}
},
scope: "regular"
}, () => {
console.log("Proxy configured:", config.host + ":" + config.port);
});
}
Comment on lines +48 to +74
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validate proxy port before applying settings.

parseInt can yield NaN or out-of-range values, which may cause a silent failure. Validate the port before calling chrome.proxy.settings.set.

✅ Proposed validation
   await chrome.storage.local.set({ proxyConfig: config, lastConfigHash: hashConfig(config) });
   cachedCredentials = config;
 
+  const port = Number.parseInt(config.port, 10);
+  if (!Number.isInteger(port) || port < 1 || port > 65535) {
+    console.error("Invalid proxy port:", config.port);
+    return;
+  }
+
   chrome.proxy.settings.set({
     value: {
       mode: "fixed_servers",
       rules: {
         singleProxy: {
           scheme: config.scheme || "http",
           host: config.host,
-          port: parseInt(config.port, 10)
+          port
         },
         bypassList: config.bypassList || ["localhost", "127.0.0.1"]
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Apply proxy settings
async function applyProxySettings(config) {
if (!config || !config.host || !config.port) {
chrome.proxy.settings.clear({ scope: "regular" });
await chrome.storage.local.set({ proxyConfig: null, lastConfigHash: null });
cachedCredentials = null;
return;
}
await chrome.storage.local.set({ proxyConfig: config, lastConfigHash: hashConfig(config) });
cachedCredentials = config;
chrome.proxy.settings.set({
value: {
mode: "fixed_servers",
rules: {
singleProxy: {
scheme: config.scheme || "http",
host: config.host,
port: parseInt(config.port, 10)
},
bypassList: config.bypassList || ["localhost", "127.0.0.1"]
}
},
scope: "regular"
});
}
// Apply proxy settings
async function applyProxySettings(config) {
if (!config || !config.host || !config.port) {
chrome.proxy.settings.clear({ scope: "regular" });
await chrome.storage.local.set({ proxyConfig: null, lastConfigHash: null });
cachedCredentials = null;
return;
}
await chrome.storage.local.set({ proxyConfig: config, lastConfigHash: hashConfig(config) });
cachedCredentials = config;
const port = Number.parseInt(config.port, 10);
if (!Number.isInteger(port) || port < 1 || port > 65535) {
console.error("Invalid proxy port:", config.port);
return;
}
chrome.proxy.settings.set({
value: {
mode: "fixed_servers",
rules: {
singleProxy: {
scheme: config.scheme || "http",
host: config.host,
port
},
bypassList: config.bypassList || ["localhost", "127.0.0.1"]
}
},
scope: "regular"
});
}
🤖 Prompt for AI Agents
In `@brightdata-proxy-extension/background.js` around lines 48 - 74, The
applyProxySettings function uses parseInt(config.port, 10) without validating
the result, so you must validate the parsed port is a finite integer in the
valid TCP range (1–65535) before calling chrome.proxy.settings.set or saving
config/cachedCredentials; if the port is invalid (NaN, non-integer, or out of
range) clear the proxy (chrome.proxy.settings.clear), set storage
proxyConfig/lastConfigHash to null, reset cachedCredentials, and optionally log
the bad config, otherwise continue to save proxyConfig/lastConfigHash, set
cachedCredentials and call chrome.proxy.settings.set with the validated integer
port.


// Handle proxy authentication
chrome.webRequest.onAuthRequired.addListener(
(details, callbackFn) => {
if (proxyConfig && proxyConfig.username && proxyConfig.password) {
console.log("Auth required, providing credentials");
callbackFn({
authCredentials: {
username: proxyConfig.username,
password: proxyConfig.password
}
});
} else {
console.log("Auth required but no credentials available");
callbackFn({ cancel: false });
}
},
{ urls: ["<all_urls>"] },
["asyncBlocking"]
);
Comment on lines 93 to 121
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Chrome extension webRequest onAuthRequired details.isProxy challenger property API documentation

💡 Result:

In chrome.webRequest.onAuthRequired, the event details object includes:

  • details.challenger: an object describing the server (or proxy) that is requesting authentication:

  • details.isProxy (boolean): indicates whether the auth challenge is coming from a proxy:

    • true = proxy auth (Proxy-Authenticate, typically HTTP 407)
    • false = origin/server auth (WWW-Authenticate, typically HTTP 401) (developer.mozilla.org)

Notes:

  • onAuthRequired is only fired for HTTP and HTTPS/TLS proxies that require authentication (not SOCKS proxies). (developer.mozilla.org)
  • In Manifest V3, Chrome requires the webRequestAuthProvider permission to use onAuthRequired. (developer.chrome.com)

Citations:


🏁 Script executed:

# Check if there are any existing security checks in the codebase
rg -i "isProxy|challenger" brightdata-proxy-extension/

Repository: reclaimprotocol/popcorn-images

Length of output: 56


🏁 Script executed:

# Look for any comments or documentation about auth handling
rg -i "auth|credential" brightdata-proxy-extension/background.js -A 2 -B 2

Repository: reclaimprotocol/popcorn-images

Length of output: 575


🏁 Script executed:

rg "proxyConfig\s*=" brightdata-proxy-extension/background.js -B 3 -A 3

Repository: reclaimprotocol/popcorn-images

Length of output: 367


🏁 Script executed:

rg "proxyConfig" brightdata-proxy-extension/background.js -B 2 -A 2 | head -40

Repository: reclaimprotocol/popcorn-images

Length of output: 615


🏁 Script executed:

rg "proxyConfig\." brightdata-proxy-extension/background.js | sort | uniq

Repository: reclaimprotocol/popcorn-images

Length of output: 227


🏁 Script executed:

# Look for the config object structure or where it's used
rg "config\." brightdata-proxy-extension/background.js -B 2 -A 2 | head -50

Repository: reclaimprotocol/popcorn-images

Length of output: 646


Restrict auth handling to proxy challenges to prevent credential leakage.

onAuthRequired fires for both HTTP proxy auth challenges (HTTP 407, isProxy=true) and origin/server auth challenges (HTTP 401, isProxy=false). Without a guard on details.isProxy, proxy credentials are leaked to any website that requests authentication. Add a check for details.isProxy === true before responding with credentials, and optionally validate the challenger host matches the proxy host.

🔒 Suggested hardening
 chrome.webRequest.onAuthRequired.addListener(
   (details, callbackFn) => {
+    if (!details.isProxy) {
+      callbackFn({ cancel: false });
+      return;
+    }
+    if (proxyConfig?.host && details.challenger?.host && details.challenger.host !== proxyConfig.host) {
+      callbackFn({ cancel: false });
+      return;
+    }
     if (proxyConfig && proxyConfig.username && proxyConfig.password) {
       console.log("Auth required, providing credentials");
       callbackFn({
         authCredentials: {
           username: proxyConfig.username,
           password: proxyConfig.password
         }
       });
     } else {
       console.log("Auth required but no credentials available");
       callbackFn({ cancel: false });
     }
   },
   { urls: ["<all_urls>"] },
   ["asyncBlocking"]
 );
🤖 Prompt for AI Agents
In `@brightdata-proxy-extension/background.js` around lines 49 - 67, The
onAuthRequired handler (chrome.webRequest.onAuthRequired) currently returns
proxyConfig credentials for any auth challenge; restrict it so you only supply
credentials when details.isProxy === true and proxyConfig exists, e.g., check
details.isProxy before calling callbackFn with authCredentials, otherwise call
callbackFn({cancel:false}); optionally also validate the challenger host in
details.challenger or details.host against your configured proxy host to avoid
sending proxy credentials to origin/server challenges.


// Initialize proxy configuration
async function init() {
const config = await fetchProxyConfig();
if (config) {
applyProxySettings(config);
}
}

init();
console.log("Bright Data Proxy Extension loaded (API-configured)");
17 changes: 17 additions & 0 deletions brightdata-proxy-extension/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "Bright Data Proxy Auth",
"version": "1.0.0",
"manifest_version": 3,
"description": "Proxy authentication for Bright Data",
"permissions": [
"proxy",
"webRequest",
"webRequestAuthProvider"
],
"host_permissions": [
"<all_urls>"
],
"background": {
"service_worker": "background.js"
}
}
17 changes: 17 additions & 0 deletions images/chromium-headful/run-docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ if [[ "$RUN_AS_ROOT" == "true" ]]; then
CHROMIUM_FLAGS_DEFAULT="$CHROMIUM_FLAGS_DEFAULT --no-sandbox --no-zygote"
fi
CHROMIUM_FLAGS="${CHROMIUM_FLAGS:-$CHROMIUM_FLAGS_DEFAULT}"

# Add proxy extension flags if proxy is enabled
if [[ "${PROXY_ENABLED:-}" == "true" ]]; then
echo "Proxy enabled: $PROXY_HOST:$PROXY_PORT"
CHROMIUM_FLAGS="$CHROMIUM_FLAGS --load-extension=/home/kernel/extensions/brightdata-proxy --disable-extensions-except=/home/kernel/extensions/brightdata-proxy"
fi

rm -rf .tmp/chromium
mkdir -p .tmp/chromium
FLAGS_FILE="$(pwd)/.tmp/chromium/flags"
Expand Down Expand Up @@ -70,6 +77,16 @@ if [[ -n "${PLAYWRIGHT_ENGINE:-}" ]]; then
RUN_ARGS+=( -e PLAYWRIGHT_ENGINE="$PLAYWRIGHT_ENGINE" )
fi

# Proxy environment variables
if [[ "${PROXY_ENABLED:-}" == "true" ]]; then
RUN_ARGS+=( -e PROXY_ENABLED=true )
RUN_ARGS+=( -e PROXY_HOST="$PROXY_HOST" )
RUN_ARGS+=( -e PROXY_PORT="$PROXY_PORT" )
[[ -n "$PROXY_USERNAME" ]] && RUN_ARGS+=( -e PROXY_USERNAME="$PROXY_USERNAME" )
[[ -n "$PROXY_PASSWORD" ]] && RUN_ARGS+=( -e PROXY_PASSWORD="$PROXY_PASSWORD" )
[[ -n "$PROXY_SCHEME" ]] && RUN_ARGS+=( -e PROXY_SCHEME="$PROXY_SCHEME" )
fi

# WebRTC port mapping
if [[ "${ENABLE_WEBRTC:-}" == "true" ]]; then
echo "Running container with WebRTC"
Expand Down
138 changes: 138 additions & 0 deletions server/cmd/api/api/proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package api

import (
"context"
"encoding/json"
"os"
"sync"

"github.com/onkernel/kernel-images/server/lib/logger"
oapi "github.com/onkernel/kernel-images/server/lib/oapi"
)

const proxyConfigPath = "/chromium/proxy-config.json"

var (
proxyConfigMu sync.RWMutex
proxyConfig *oapi.ProxyConfig
)

// GetProxyConfig returns the current proxy configuration.
func (s *ApiService) GetProxyConfig(ctx context.Context, _ oapi.GetProxyConfigRequestObject) (oapi.GetProxyConfigResponseObject, error) {
log := logger.FromContext(ctx)

proxyConfigMu.RLock()
defer proxyConfigMu.RUnlock()

// If we have a cached config, return it
if proxyConfig != nil {
log.Info("returning cached proxy config", "host", stringVal(proxyConfig.Host))
return oapi.GetProxyConfig200JSONResponse(*proxyConfig), nil
}

// Try to load from file
data, err := os.ReadFile(proxyConfigPath)
if err != nil {
if os.IsNotExist(err) {
// Return empty config if file doesn't exist
log.Info("no proxy config found, returning empty config")
return oapi.GetProxyConfig200JSONResponse(oapi.ProxyConfig{}), nil
}
log.Error("failed to read proxy config", "error", err)
return oapi.GetProxyConfig500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to read proxy config"}}, nil
}

var cfg oapi.ProxyConfig
if err := json.Unmarshal(data, &cfg); err != nil {
log.Error("failed to parse proxy config", "error", err)
return oapi.GetProxyConfig500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to parse proxy config"}}, nil
}

log.Info("returning proxy config from file", "host", stringVal(cfg.Host))
return oapi.GetProxyConfig200JSONResponse(cfg), nil
}

// SetProxyConfig sets the proxy configuration.
func (s *ApiService) SetProxyConfig(ctx context.Context, request oapi.SetProxyConfigRequestObject) (oapi.SetProxyConfigResponseObject, error) {
log := logger.FromContext(ctx)

if request.Body == nil {
return oapi.SetProxyConfig400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "request body required"}}, nil
}

cfg := request.Body

// Validate required fields
if cfg.Host == nil || *cfg.Host == "" {
return oapi.SetProxyConfig400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "host is required"}}, nil
}
if cfg.Port == nil || *cfg.Port == 0 {
return oapi.SetProxyConfig400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "port is required"}}, nil
}
Comment on lines +65 to +71
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validate proxy port range (1–65535).

Line 69 accepts negative or >65535 ports, which will serialize but fail downstream. Add a range check.

✅ Proposed fix
-	if cfg.Port == nil || *cfg.Port == 0 {
+	if cfg.Port == nil || *cfg.Port < 1 || *cfg.Port > 65535 {
 		return oapi.SetProxyConfig400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "port is required"}}, nil
 	}
🤖 Prompt for AI Agents
In `@server/cmd/api/api/proxy.go` around lines 65 - 71, The port validation
currently only checks for nil or zero and allows negative or >65535 values;
update the cfg.Port validation (the same block that returns
oapi.SetProxyConfig400JSONResponse) to also reject values outside the valid TCP
port range (1–65535) by checking *cfg.Port >= 1 && *cfg.Port <= 65535 and
returning a BadRequest response (oapi.SetProxyConfig400JSONResponse with an
appropriate message like "port must be between 1 and 65535") when the check
fails.


// Set default scheme if not provided
if cfg.Scheme == nil {
defaultScheme := oapi.Http
cfg.Scheme = &defaultScheme
}

// Set default bypass list if not provided
if cfg.BypassList == nil {
cfg.BypassList = &[]string{"localhost", "127.0.0.1"}
}

proxyConfigMu.Lock()
defer proxyConfigMu.Unlock()

// Save to file
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
log.Error("failed to marshal proxy config", "error", err)
return oapi.SetProxyConfig500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to marshal proxy config"}}, nil
}

// Ensure the directory exists
if err := os.MkdirAll("/chromium", 0o755); err != nil {
log.Error("failed to create chromium dir", "error", err)
return oapi.SetProxyConfig500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to create chromium dir"}}, nil
}

if err := os.WriteFile(proxyConfigPath, data, 0o644); err != nil {
log.Error("failed to write proxy config", "error", err)
return oapi.SetProxyConfig500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to write proxy config"}}, nil
}
Comment on lines +95 to +103
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Restrict proxy config file permissions to protect credentials.

Line 100 writes with 0644, which makes proxy credentials world-readable on the container filesystem. Use 0600 (or at least 0640) to limit exposure.

🔐 Proposed fix
-	if err := os.WriteFile(proxyConfigPath, data, 0o644); err != nil {
+	if err := os.WriteFile(proxyConfigPath, data, 0o600); err != nil {
 		log.Error("failed to write proxy config", "error", err)
 		return oapi.SetProxyConfig500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to write proxy config"}}, nil
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if err := os.MkdirAll("/chromium", 0o755); err != nil {
log.Error("failed to create chromium dir", "error", err)
return oapi.SetProxyConfig500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to create chromium dir"}}, nil
}
if err := os.WriteFile(proxyConfigPath, data, 0o644); err != nil {
log.Error("failed to write proxy config", "error", err)
return oapi.SetProxyConfig500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to write proxy config"}}, nil
}
if err := os.MkdirAll("/chromium", 0o755); err != nil {
log.Error("failed to create chromium dir", "error", err)
return oapi.SetProxyConfig500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to create chromium dir"}}, nil
}
if err := os.WriteFile(proxyConfigPath, data, 0o600); err != nil {
log.Error("failed to write proxy config", "error", err)
return oapi.SetProxyConfig500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to write proxy config"}}, nil
}
🤖 Prompt for AI Agents
In `@server/cmd/api/api/proxy.go` around lines 95 - 103, The proxy config is being
written with world-readable permissions; update the file and directory creation
calls so credentials are locked down: change the os.WriteFile call that writes
proxyConfigPath from mode 0o644 to 0o600 (or 0o640 if group read is required)
and tighten the os.MkdirAll directory mode (e.g., from 0o755 to 0o700) to reduce
access; locate these changes around the os.MkdirAll("/chromium", ...) and
os.WriteFile(proxyConfigPath, data, ...) lines in proxy.go and adjust the octal
permission arguments accordingly.


// Update cache
proxyConfig = cfg

log.Info("proxy config saved", "host", *cfg.Host, "port", *cfg.Port)
return oapi.SetProxyConfig200JSONResponse(*cfg), nil
}

// DeleteProxyConfig clears the proxy configuration.
func (s *ApiService) DeleteProxyConfig(ctx context.Context, _ oapi.DeleteProxyConfigRequestObject) (oapi.DeleteProxyConfigResponseObject, error) {
log := logger.FromContext(ctx)

proxyConfigMu.Lock()
defer proxyConfigMu.Unlock()

// Clear cache
proxyConfig = nil

// Remove file
if err := os.Remove(proxyConfigPath); err != nil && !os.IsNotExist(err) {
log.Error("failed to remove proxy config file", "error", err)
return oapi.DeleteProxyConfig500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to remove proxy config file"}}, nil
}

log.Info("proxy config cleared")
return oapi.DeleteProxyConfig204Response{}, nil
}

// stringVal returns the value of a string pointer or empty string if nil
func stringVal(s *string) string {
if s == nil {
return ""
}
return *s
}
2 changes: 1 addition & 1 deletion server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
github.com/go-chi/chi/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/kelseyhightower/envconfig v1.4.0
github.com/klauspost/compress v1.18.3
github.com/m1k1o/neko/server v0.0.0-20251008185748-46e2fc7d3866
github.com/nrednav/cuid2 v1.1.0
github.com/oapi-codegen/runtime v1.1.2
Expand All @@ -33,7 +34,6 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.18.3 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
Expand Down
Loading