Skip to content

[APPS-2953] Fix: warn once when the dev server refuses a host - #597

Open
tyffical wants to merge 1 commit into
tiffany.trinh/apps-2962-local-app-cspfrom
tiffany.trinh/apps-2953-guard-followups
Open

tyffical wants to merge 1 commit into
tiffany.trinh/apps-2962-local-app-cspfrom
tiffany.trinh/apps-2953-guard-followups

Conversation

@tyffical

@tyffical tyffical commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Changes

7 changes across dev-server.ts, dev-server-request-guard.ts, README.md and their tests
What changed File
Warns the first time it logs a refusal's message, then logs repeats at debug. dev-server.ts
Caps warnings at MAX_WARNED_REJECTIONS (32) per dev server, with one notice. dev-server.ts
Escapes a refused hostname's non-ASCII characters, so a Host can't send control bytes to the terminal. dev-server-request-guard.ts
Moves the host-not-allowed message's server.allowedHosts advice into ALLOWED_HOSTS_HINT. dev-server-request-guard.ts
Tests warn-once, the cap, per-instance state and credential-free logs. dev-server.test.ts
Tests the hostname escaping and that a refused-host message ends with ALLOWED_HOSTS_HINT. dev-server-request-guard.test.ts
Documents the warn-once logging. README.md

QA Instructions

yarn install && yarn build:all
# Expected: exit 0

yarn typecheck:all && yarn lint && yarn cli integrity
# Expected: all exit 0, integrity leaves no changes to commit

yarn test:unit packages/plugins/apps/src/vite/dev-server-request-guard.test.ts packages/plugins/apps/src/vite/dev-server.test.ts
# Expected: both files pass

yarn test:unit
# Expected on macOS: only manifest-tags.integration and parameters.integration fail, because /var is a symlink to /private/var

TMPDIR=/private/tmp yarn test:unit packages/plugins/apps/src/vite/manifest-tags.integration.test.ts packages/plugins/apps/src/vite/parameters.integration.test.ts
# Expected: both files pass
  • A real vite dev run (Vite 6.4.3) of the request guard, with outbound sockets blocked and no Datadog credentials:
Check Result Evidence
Refused host, 3 times and once on another port HTTP 403 each with only the server.allowedHosts hint; one warn, the 3 repeats at debug only qa-guard-logging.sh
Host with C1 control bytes HTTP 403; the hostname prints each C1 byte as a JSON \u escape, with no raw non-ASCII byte in the terminal qa-guard-logging.sh
Cross-origin call, twice via localhost and once via 127.0.0.1 HTTP 403 each; one warn, the 2 repeats at debug only qa-guard-logging.sh
Wrong Content-Type HTTP 415 qa-guard-logging.sh
Same-origin /__dd/debugBundle call HTTP 200 with the bundle qa-guard-logging.sh
34 more refused hosts 29 warns, the last (cap-28) with the cap notice; the other 5 at debug only qa-guard-logging.sh, both log levels
Real vite dev run: script, app files and output
  • Put the files below in one directory, then run:
W=<build-plugins checkout with this branch built> ./qa-guard-logging.sh
W=<build-plugins checkout with this branch built> QA_LOG_LEVEL=debug ./qa-guard-logging.sh
  • qa-guard-logging.sh:
#!/bin/bash
# Run: W=<build-plugins checkout with this branch built> [VITE_BIN=<vite.js>] [QA_LOG_LEVEL=debug] ./qa-guard-logging.sh
set -u
cd "$(dirname "$0")"; QA_DIR=$PWD
PORT=5297
BASE="http://127.0.0.1:${PORT}"
VITE_BIN=${VITE_BIN:-$W/node_modules/vite/bin/vite.js}
LOG="vite-dev-${QA_LOG_LEVEL:-warn}.log"
(cd app && exec env -u DD_API_KEY -u DD_APP_KEY -u DATADOG_API_KEY -u DATADOG_APP_KEY -u DD_OAUTH_ACCESS_TOKEN -u DATADOG_OAUTH_ACCESS_TOKEN \
    NODE_OPTIONS="-r $QA_DIR/loopback-only.cjs" W="$W" node "$VITE_BIN" > "../$LOG" 2>&1) &
PID=$!
echo "== vite pid $PID, $(node "$VITE_BIN" --version 2>/dev/null)"
for _ in $(seq 1 90); do grep -q 'Local:' "$LOG" && break; sleep 1; done
FN=$(node -e "const c=require('crypto');console.log(c.createHash('sha256').update('backend/greet').digest('hex')+'.greet')")
# Vite registers backend functions when it transforms the importing module.
curl -s -o /dev/null "${BASE}/main.js" -H "Host: localhost:${PORT}"
curl -s -o /dev/null "${BASE}/backend/greet.backend.ts" -H "Host: localhost:${PORT}"
post() { # $1 label, $2 route, $3 Host, $4 Origin, $5 Content-Type
    local out
    out=$(curl -s -w '\n%{http_code}' -X POST "${BASE}$2" -H "Host: $3" -H "Origin: $4" \
        -H "Content-Type: $5" --data "{\"functionName\":\"${FN}\",\"args\":[\"x\"]}")
    printf '%-26s %s  %s\n' "$1" "${out##*$'\n'}" "$(printf '%s' "${out%$'\n'*}" | tr '\n' ' ' | cut -c1-230)"
}
OWN="localhost:${PORT}"
echo "== Responses"
for i in 1 2 3; do post "refused host #$i" /__dd/executeAction "evil.example" "http://evil.example" application/json; done
post "refused host, other port" /__dd/executeAction "evil.example:8080" "http://evil.example:8080" application/json
post "C1 bytes in Host" /__dd/executeAction "$(printf 'evil\x9b2J\x85.example')" "http://x.example" application/json | LC_ALL=C sed 's/[^[:print:]]/?/g'
for i in 1 2; do post "cross-origin #$i" /__dd/executeAction "$OWN" "http://attacker.example" application/json; done
post "cross-origin, 127.0.0.1" /__dd/executeAction "127.0.0.1:${PORT}" "http://attacker.example" application/json
post "wrong content-type" /__dd/executeAction "$OWN" "http://${OWN}" text/plain
post "same origin" /__dd/debugBundle "$OWN" "http://${OWN}" application/json
for i in $(seq 0 33); do
    post "cap host #$i" /__dd/executeAction "cap-${i}.example:${PORT}" "http://cap-${i}.example:${PORT}" application/json > /dev/null
done
sleep 1
kill "$PID"; wait 2>/dev/null
plain() { sed 's/\x1b\[[0-9;]*m//g' "$LOG"; }
echo "== Dev server terminal, excluding the cap hosts"
plain | grep -a -E 'Rejected|BLOCKED' | grep -v 'cap-[0-9]*\.example'
echo "== Raw non-ASCII bytes in rejection lines: $(plain | grep -a 'Rejected' | LC_ALL=C grep -c '[^ -~]')"
echo "== Rejection lines by level"
for level in warn debug; do
    printf '%-5s refused-host=%s  cross-origin=%s  cap-hosts=%s\n' "$level" \
        "$(plain | grep -c "\[$level|vite|apps\] Rejected.*\"evil.example\"")" \
        "$(plain | grep -c "\[$level|vite|apps\] Rejected.*own origin")" \
        "$(plain | grep -c "\[$level|vite|apps\] Rejected.*cap-[0-9]*\.example")"
done
echo "== Cap notice"
plain | grep -a "Further rejections" | sed -E 's/^(\[[^]]*\]).*This host \(("[^"]*")\).*(Further rejections.*)/\1 host \2 ... \3/'
  • app/index.html:
<!doctype html>
<html><body><script type="module" src="/main.js"></script></body></html>
  • app/main.js:
import { greet } from './backend/greet.backend.ts';
window.__greet = greet;
  • app/backend/greet.backend.ts:
export async function greet(name: string) {
    return `hello ${name}`;
}
  • app/vite.config.mjs:
// The built plugin is loaded from the checkout named by W.
const { datadogVitePlugin } = await import(`${process.env.W}/packages/published/vite-plugin/dist/src/index.mjs`);

export default {
    server: { host: '127.0.0.1', port: 5297, strictPort: true },
    plugins: [
        datadogVitePlugin({ logLevel: process.env.QA_LOG_LEVEL ?? 'warn', apps: { enable: true } }),
    ],
};
  • loopback-only.cjs:
// Preloaded with `node -r`: fails and logs any outbound socket that isn't loopback.
const net = require('net');
const original = net.Socket.prototype.connect;
net.Socket.prototype.connect = function (...args) {
    const options = typeof args[0] === 'object' ? args[0] : { port: args[0], host: args[1] };
    const host = options.host ?? 'localhost';
    if (!options.path && !['127.0.0.1', '::1', 'localhost'].includes(host)) {
        console.log(`[qa] BLOCKED outbound connection to ${host}:${options.port}`);
        process.nextTick(() => this.destroy(new Error(`blocked outbound connection to ${host}`)));
        return this;
    }
    return original.apply(this, args);
};
  • Output at the default log level; GitHub rewrites C1 escape text in PR bodies, so each escape below has a space after \u:
== Responses
refused host #1            403  {"success":false,"error":"Blocked request. This host (\"evil.example\") is not allowed. If you trust this host, add it to `server.allowedHosts` in vite.config.js."}
refused host #2            403  {"success":false,"error":"Blocked request. This host (\"evil.example\") is not allowed. If you trust this host, add it to `server.allowedHosts` in vite.config.js."}
refused host #3            403  {"success":false,"error":"Blocked request. This host (\"evil.example\") is not allowed. If you trust this host, add it to `server.allowedHosts` in vite.config.js."}
refused host, other port   403  {"success":false,"error":"Blocked request. This host (\"evil.example\") is not allowed. If you trust this host, add it to `server.allowedHosts` in vite.config.js."}
C1 bytes in Host           403  {"success":false,"error":"Blocked request. This host (\"evil\\u 009b2J\\u 0085.example\") is not allowed. If you trust this host, add it to `server.allowedHosts` in vite.config.js."}
cross-origin #1            403  {"success":false,"error":"Blocked request. Only the dev server's own origin can call this endpoint."}
cross-origin #2            403  {"success":false,"error":"Blocked request. Only the dev server's own origin can call this endpoint."}
cross-origin, 127.0.0.1    403  {"success":false,"error":"Blocked request. Only the dev server's own origin can call this endpoint."}
wrong content-type         415  {"success":false,"error":"Requests must use Content-Type: application/json."}
same origin                200  var s=Object.defineProperty;var c=(t,o)=>s(t,"name",{value:o,configurable:!0});async function u(t){return`hello ${t}`}c(u,"greet");async function h(t){globalThis.$=t,typeof setExecuteActionImplementation=="function"&&setExecuteAct
== Dev server terminal, excluding the cap hosts
[qa] BLOCKED outbound connection to browser-http-intake.logs.datadoghq.com:443
[warn|vite|apps] Rejected POST /__dd/executeAction: Blocked request. This host ("evil.example") is not allowed. If you trust this host, add it to `server.allowedHosts` in vite.config.js.
[warn|vite|apps] Rejected POST /__dd/executeAction: Blocked request. This host ("evil\u 009b2J\u 0085.example") is not allowed. If you trust this host, add it to `server.allowedHosts` in vite.config.js.
[warn|vite|apps] Rejected POST /__dd/executeAction: Blocked request. Only the dev server's own origin can call this endpoint.
[warn|vite|apps] Rejected POST /__dd/executeAction: Requests must use Content-Type: application/json.
== Raw non-ASCII bytes in rejection lines: 0
== Rejection lines by level
warn  refused-host=1  cross-origin=1  cap-hosts=29
debug refused-host=0  cross-origin=0  cap-hosts=0
== Cap notice
[warn|vite|apps] host "cap-28.example" ... Further rejections are only logged with the Datadog plugin's `logLevel: 'debug'`.
  • With QA_LOG_LEVEL=debug, the responses and cap notice match the default run; the terminal and counts become:
== Dev server terminal, excluding the cap hosts
[qa] BLOCKED outbound connection to browser-http-intake.logs.datadoghq.com:443
[warn|vite|apps] Rejected POST /__dd/executeAction: Blocked request. This host ("evil.example") is not allowed. If you trust this host, add it to `server.allowedHosts` in vite.config.js.
[debug|vite|apps] Rejected POST /__dd/executeAction: Blocked request. This host ("evil.example") is not allowed. If you trust this host, add it to `server.allowedHosts` in vite.config.js.
[debug|vite|apps] Rejected POST /__dd/executeAction: Blocked request. This host ("evil.example") is not allowed. If you trust this host, add it to `server.allowedHosts` in vite.config.js.
[debug|vite|apps] Rejected POST /__dd/executeAction: Blocked request. This host ("evil.example") is not allowed. If you trust this host, add it to `server.allowedHosts` in vite.config.js.
[warn|vite|apps] Rejected POST /__dd/executeAction: Blocked request. This host ("evil\u 009b2J\u 0085.example") is not allowed. If you trust this host, add it to `server.allowedHosts` in vite.config.js.
[warn|vite|apps] Rejected POST /__dd/executeAction: Blocked request. Only the dev server's own origin can call this endpoint.
[debug|vite|apps] Rejected POST /__dd/executeAction: Blocked request. Only the dev server's own origin can call this endpoint.
[debug|vite|apps] Rejected POST /__dd/executeAction: Blocked request. Only the dev server's own origin can call this endpoint.
[warn|vite|apps] Rejected POST /__dd/executeAction: Requests must use Content-Type: application/json.
== Raw non-ASCII bytes in rejection lines: 0
== Rejection lines by level
warn  refused-host=1  cross-origin=1  cap-hosts=29
debug refused-host=3  cross-origin=2  cap-hosts=5
  • Four refusal messages plus 28 cap hosts fill the 32-entry set, so cap-28 carries the notice.

Blast Radius

  • Only the apps plugin's dev-server /__dd routes change.
  • A refusal's first occurrence prints a warn line, at most 33 per dev server.
  • No feature flag. Risk: Low.

Documentation

🤖 Generated with Claude Code

@tyffical
tyffical force-pushed the tiffany.trinh/apps-2953-guard-followups branch from 6006175 to 2a84069 Compare October 16, 2026 18:50
@tyffical
tyffical requested a balanced review from Copilot October 16, 2026 19:14
@tyffical

Copy link
Copy Markdown
Contributor Author

@codex review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The focused logging and message changes have targeted test coverage and no identified blocking issues.

0 open findings

What changed in this PR

Improves visibility of rejected requests to the apps plugin’s Vite dev-server routes without flooding default logs.

Changes:

  • Warns once per Host/reason pair, with a per-server cap and debug logging afterward.
  • Adds stable rejection reasons and upgrade guidance for older Vite versions.
  • Tests deduplication, warning limits, instance isolation, and credential exclusion.
File Description
packages/​plugins/​apps/​src/​vite/​dev-server.ts Adds bounded, per-server refusal warnings.
packages/​plugins/​apps/​src/​vite/​dev-server.test.ts Tests refusal logging and deduplication.
packages/​plugins/​apps/​src/​vite/​dev-server-request-guard.ts Adds rejection reasons, shared Host extraction, and upgrade guidance.
packages/​plugins/​apps/​src/​vite/​dev-server-request-guard.test.ts Verifies rejection reasons and guidance.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit: 2a840694e8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-16T19:16:46.073744Z 2a84069 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@tyffical
tyffical requested a review from Scott-Meyer October 16, 2026 19:30
@tyffical
tyffical marked this pull request as ready for review October 16, 2026 19:30
@tyffical
tyffical requested a review from a team as a code owner October 16, 2026 19:30
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2953-guard-followups branch 2 times, most recently from 22c3417 to 3db0670 Compare October 26, 2026 23:19
@tyffical tyffical changed the title [APPS-2953] Fix: warn once when the dev server refuses a host, and hint at upgrading old Vite [APPS-2953] Fix: warn once when the dev server refuses a host Oct 26, 2026
@datadog-datadog-prod-us1

datadog-datadog-prod-us1 Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Pipelines  Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: d8a533d | Docs | View more details | Give us feedback!

@tyffical
tyffical force-pushed the tiffany.trinh/apps-2953-guard-followups branch 2 times, most recently from 6d804f4 to f35b888 Compare October 28, 2026 16:27
@tyffical
tyffical changed the base branch from master to tiffany.trinh/apps-2962-local-app-csp October 28, 2026 16:27
@github-actions

Copy link
Copy Markdown

Live Debugger Runtime Benchmark

SDK-loaded dormant-probe runtime overhead, measured against an uninstrumented bundle in the same browser session.

Browser Workload Quality Per-call overhead upper
chrome Hot clean <= 2.56 ns
chrome Tiny clean <= 0.02 ns
firefox Hot clean <= 9.47 ns
firefox Tiny clean <= 0.04 ns
safari Hot clean <= 5.54 ns
safari Tiny clean <= 0.97 ns

What do the Tiny and Hot workloads represent?

Browser Debugger SDK: 7.16.2 · built 2026-10-27 · S3 ETag 872de873

Full diagnostics
browser  workload  quality  per-call overhead upper  overhead upper          95% CI        A/A diag        block CI  acf(1)   baseline  instrumented                         samples
-------  --------  -------  -----------------------  --------------  --------------  --------------  --------------  ------  ---------  ------------  ------------------------------
chrome   Hot       clean                 <= 2.56 ns        <= 5.14%   2.52..2.56 ns  -0.02..0.01 ns   2.51..2.56 ns    0.21  52.683 ms     55.365 ms   102 (trim 20%, outliers 4.9%)
chrome   Tiny      clean                 <= 0.02 ns        <= 0.24%  -0.03..0.01 ns  -0.01..0.03 ns  -0.03..0.02 ns    0.03  56.540 ms     56.583 ms  102 (trim 20%, outliers 13.7%)
firefox  Hot       clean                 <= 9.47 ns       <= 37.59%   9.44..9.47 ns  -0.01..0.00 ns   9.44..9.47 ns   -0.01  40.200 ms     55.300 ms   102 (trim 20%, outliers 7.8%)
firefox  Tiny      clean                 <= 0.04 ns        <= 0.24%   0.02..0.04 ns  -0.00..0.01 ns   0.02..0.03 ns    0.06  53.760 ms     53.860 ms  102 (trim 20%, outliers 10.8%)
safari   Hot       clean                 <= 5.54 ns       <= 22.11%   5.52..5.54 ns  -0.01..0.01 ns   5.52..5.54 ns   -0.20  45.390 ms     55.400 ms   102 (trim 20%, outliers 4.9%)
safari   Tiny      clean                 <= 0.97 ns       <= 25.97%   0.96..0.97 ns  -0.00..0.00 ns   0.96..0.97 ns    0.23  41.120 ms     51.770 ms   102 (trim 20%, outliers 3.9%)

Raw samples are in the live-debugger-runtime-bench-results artifact.

@tyffical
tyffical force-pushed the tiffany.trinh/apps-2962-local-app-csp branch from 66c1dde to fca924d Compare October 28, 2026 16:52
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2953-guard-followups branch from f35b888 to 025a10b Compare October 28, 2026 16:52
@tyffical
tyffical requested review from oliverli and removed request for Scott-Meyer October 28, 2026 16:59
A refused /__dd request now logs at warn the first time a dev server
logs that refusal's message, and at debug after that, so a developer
behind a tunnel or LAN hostname sees why the request failed. The set of
warned messages is capped per dev server, with one notice once the cap
is reached. A refused hostname's non-ASCII characters are escaped,
since the message now reaches the terminal by default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2962-local-app-csp branch from fca924d to 6c87182 Compare October 28, 2026 17:39
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2953-guard-followups branch from 025a10b to d8a533d Compare October 28, 2026 17:39
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.

2 participants