security: harden proxy trust + model mass-assignment + terminal-token log hygiene
Fixes from the security audit (the brute-force/rate-limiting angle matters more now that the password policy is min(6)/no-complexity, so IP-keyed throttles must not be spoofable): - bootstrap/app.php: trustProxies(at: '*') → trust only PRIVATE ranges in prod. Caddy reaches app:80 from the docker bridge (private IP) so it stays trusted, but a public/off-network source can no longer forge X-Forwarded-For to spoof request()->ip() and bypass the login/2FA/forgot throttles + the brute-force ban. - TerminalSession + HostCredential: replace $guarded=[] with an explicit $fillable allowlist; add $hidden=[secret,passphrase] to HostCredential so it never serializes its credential fields. - nginx /terminal/ws: access_log off — the single-use session token rode the query string into the access log. Verified: full suite 467 pass; all panel pages 200 with zero console errors; server terminal still connects + runs commands. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/v1-foundation
parent
f3d2a9592e
commit
e38409bf31
|
|
@ -11,7 +11,11 @@ use Illuminate\Database\Eloquent\Model;
|
|||
*/
|
||||
class HostCredential extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
/** Explicit allowlist (never $guarded=[]) so request input can't set unintended columns. */
|
||||
protected $fillable = ['host', 'port', 'username', 'auth_type', 'secret', 'passphrase'];
|
||||
|
||||
/** Never serialize the credential fields (mirrors SshCredential). */
|
||||
protected $hidden = ['secret', 'passphrase'];
|
||||
|
||||
protected $casts = [
|
||||
'port' => 'integer',
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ class TerminalSession extends Model
|
|||
{
|
||||
public const UPDATED_AT = null;
|
||||
|
||||
protected $guarded = [];
|
||||
/** Explicit allowlist (never $guarded=[]) so request input can't set unintended columns. */
|
||||
protected $fillable = ['token', 'user_id', 'kind', 'server_id', 'expires_at', 'used_at', 'created_at'];
|
||||
|
||||
protected $casts = [
|
||||
'expires_at' => 'datetime',
|
||||
|
|
|
|||
|
|
@ -28,8 +28,13 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||
// the sole peer. In dev the app port is published on the host, so trusting forged
|
||||
// proxy headers would let a LAN peer fake HTTPS (forcing a Secure cookie over
|
||||
// plain HTTP and breaking the bare-IP recovery login).
|
||||
//
|
||||
// Trust only PRIVATE ranges, not '*': Caddy reaches app:80 from the docker bridge (a private
|
||||
// IP), so it stays trusted, but anything arriving from a public source (a stray published
|
||||
// port, an off-network relay) can no longer forge X-Forwarded-For to spoof request()->ip()
|
||||
// and defeat the IP-keyed login/2FA/forgot throttles + the brute-force ban.
|
||||
if (($_SERVER['APP_ENV'] ?? getenv('APP_ENV') ?: 'local') === 'production') {
|
||||
$middleware->trustProxies(at: '*');
|
||||
$middleware->trustProxies(at: ['127.0.0.1/32', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']);
|
||||
}
|
||||
|
||||
// PanelScheme first (scheme/host enforcement before auth), so an HTTP request to
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ http {
|
|||
# Terminal sidecar websocket (xterm.js ↔ PTY). Same origin; the sidecar authorizes the
|
||||
# single-use token before opening the PTY. Long read timeout — sessions are long-lived.
|
||||
location = /terminal/ws {
|
||||
# The single-use session token rides the query string; don't write it to the access log.
|
||||
access_log off;
|
||||
resolver 127.0.0.11 valid=10s;
|
||||
set $terminal_upstream terminal:3000;
|
||||
proxy_pass http://$terminal_upstream;
|
||||
|
|
|
|||
Loading…
Reference in New Issue