Skip to content

Pre-launch security audit: 29 fixes - #89

Merged
drk1rd merged 19 commits into
mainfrom
security-audit
Sep 30, 2026
Merged

drk1rd merged 19 commits into
mainfrom
security-audit

Conversation

@drk1rd

@drk1rd drk1rd commented Sep 30, 2026

Copy link
Copy Markdown
Member

A security review before HomeCloud goes on public servers. Findings and reasoning: docs/security-audit-2026-10.md.

High-severity fixes:

  • The AWS handler authenticates before buffering request bodies (was up to 100 MB unauthenticated); SigV4 requires signed host and x-amz-target.
  • API Gateway HTTP_PROXY integrations can't reach loopback, metadata or the Docker bridge; the outbound blocklist covers IPv6-embedded IPv4, metadata and host-local addresses.
  • Inline S3 object views no longer expose the session token; clickjacking headers on the console.
  • iam:PassRole is enforced for ECS task definitions (native and CloudFormation), EC2 instance profiles, launch templates and Auto Scaling, judged on the resolved role ARN.
  • S3 website hosting evaluates the bucket policy per key.
  • Function URL, HTTP API and website responses can't replace the sandbox CSP.

Also: sign-in throttling before the password check and no user-existence timing leak, open redirect after sign-in, Lambda decompression bombs, internal error text no longer returned to clients or CloudTrail, ECR pull permissions for local images, DeleteObjects key validation, Cognito guess and sign-up limits, SigV2 sub-resource signing, SNS dead-letter permission, access keys refused in ?access_token, session tokens can't renew themselves, object-lock header authorization, lambda-code bucket routing.

Accepted risks are listed in the report with reasoning. VM instance code (PR #82) was out of scope and needs its own review.

Each fix has a regression test (except the console redirect, checked by hand); the full suite passes locally with Docker.

drk1rd added 19 commits October 1, 2026 00:07
…d x-amz-target to be signed

The AWS handler read up to 100 MB of body before looking at the signature, so
anyone could make the server buffer that much per connection. It now checks the
signature's freshness and access key first, verifies the signature before
reading the body when the client declared the payload hash, and caps unsigned
(public operation) bodies at 1 MB. SigV4 requests that do not sign host, or
x-amz-target when present, are rejected as AWS does.
…public request bodies

The console sign-in counted a failure only after verifying the password, so a
burst of parallel guesses all passed the throttle; it now reserves a slot first
and gives it back on success. Unknown users pay the same bcrypt cost as known
ones. Sign-in and Cognito public routes cap their JSON body at 64 KB instead of
64 MB, and servers get an idle timeout and a header size limit.
…et blocklist

HTTP_PROXY integrations used a default HTTP client, so anyone able to create an
integration could read loopback services, the VPS provider's metadata service
or the Docker bridge through a public API. They now use core.SafeClient, which
checks the resolved address at dial time, ignores proxy environment variables
and does not follow redirects. The shared blocklist now also covers
IPv4-mapped/NAT64/6to4 forms, 0.0.0.0/8, non-link-local metadata addresses and
every address of the host itself; HOMECLOUD_DENY_PRIVATE_TARGETS=1 also blocks
RFC 1918 and unique-local ranges.
…ion registration

The native task definition route (and CloudFormation, which uses it) accepted a
task role without any PassRole check, so a caller with only ecs:RegisterTaskDefinition
and ecs:RunTask could run a container holding any role that trusts ecs-tasks.
The check now lives in registerTaskDef and is made against the role's real ARN,
so alternative spellings such as role/x-/admin cannot slip past a scoped policy.
…very launch path

The native RunInstances route, launch templates and Auto Scaling groups
launching from a template accepted any instance profile without iam:PassRole
(only the AWS RunInstances call checked it), letting a caller with
ec2:RunInstances boot an instance holding any role's credentials through IMDS.
The check is now shared (ec2.PassProfile) and applied on the native route, when
a launch template carries a profile, and when a group is created or updated from
such a template.
…rget ARNs

Permission checks used the resource string exactly as the caller wrote it, so a
Deny or scoped Allow on role/admin could be dodged with the bare name or an ARN
with another path part (role/dev-/admin) that the IAM lookup still resolved to
admin. Principal.Permits now canonicalizes the resource and IAM resolves role
references to the real role ARN for iam:PassRole. Delivery to EventBridge,
scheduler, CloudWatch and Step Functions targets, and Secrets Manager rotation
functions, now rejects ARNs of another account or region instead of delivering
to the same-named local resource.
A RedrivePolicy only had to name an existing queue, so a caller with
sns:SetSubscriptionAttributes could make failed deliveries write attacker-chosen
messages into any queue, including ones they cannot send to. Naming a
dead-letter queue now needs sqs:SendMessage on it, like subscribing a queue.
Lambda container functions and ECS task definitions pulled from HomeCloud's
registry (ECR-style or localhost URIs) without any ecr: check, so any user able
to create a function or task could run, and read the contents of, another
team's private repository. Both now need ecr:BatchGetImage and
ecr:GetDownloadUrlForLayer on the repository.
…clickjacking headers

The console opens an object inline with ?access_token=<session> in the URL, and
the response was sandboxed with allow-scripts, so script inside an uploaded HTML
object could read its own location and send the viewer's session token away.
Inline views now run no scripts and load nothing from elsewhere, API responses
send Referrer-Policy: no-referrer, and the console itself is served with
X-Frame-Options/frame-ancestors so it cannot be framed.
…themselves

?access_token= accepted a full accessKeyId:secret, which would land in access
logs and browser history; only console session tokens may travel in the query
string now. GetSessionToken also refuses temporary credentials, as AWS does, so a
stolen session cannot be renewed indefinitely.
…upload

Packages were only checked for being a valid zip, then unpacked in memory at
every cold start (and copied a second time into a tar), so a tiny archive of
zeros could make each invocation allocate hundreds of megabytes. checkZip now
sums the declared uncompressed sizes against the 250 MB limit and caps the entry
count, and unzip reads no more than the remaining budget per file.
…paths)

safeNext refused "//host" but let "/\\evil.example" and "/<tab>/evil.example"
through; browsers read both as "//evil.example", so a crafted sign-in link
sent the user to another site right after entering their password. The path is
now rejected on backslashes and control characters and must resolve to this
origin.
Unexpected errors (file paths, Docker daemon output, database errors) were
copied into 500 responses, SQS batch results and, through the error message of
the trail, into other users' CloudTrail LookupEvents. Clients now get a fixed
message and the detail stays in the server log.
…/lambda-code/ out of S3

Website hosting decided that a bucket was public by looking for the strings
"*" and s3:GetObject in MinIO's copy of the policy, then served every key with
the server's storage credentials: a policy granting one prefix, or carrying a
Deny or a condition, exposed the whole bucket. Each requested key is now
evaluated against the bucket policy as an anonymous caller. DeleteObjects body
keys get the same dot-segment and length checks as URL keys, and
/lambda-code/ and /_s3/ are never claimed by anonymous S3 routing (a bucket
named lambda-code broke code downloads).
Functions and HTTP_PROXY upstreams choose their own response headers, and
respond() copied them over the sandbox Content-Security-Policy, so a function
could serve a page with script on the console's origin and take over the session
of an administrator who opened its URL. The sandbox and nosniff headers are now
re-applied when the response is written.
The sign-in throttle was keyed by pool, user and source address, so rotating
addresses had no per-account limit, and self sign-up (a bcrypt hash and a store
write per call) had none at all. Password and SRP sign-in now also count
attempts per account across addresses, and each pool accepts a bounded number of
self sign-ups per window.
…lock headers

A SigV2 signature covers only the sub-resources SigV2 defines, so whoever held a
presigned URL could append ?retention, ?publicAccessBlock and similar and have
the signer's permissions applied to the changed operation; those requests are
now refused. Uploads that set retention or a legal hold through headers now
need s3:PutObjectRetention / s3:PutObjectLegalHold, as in AWS.
@coderabbitai

coderabbitai Bot commented Sep 30, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 39 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 53b81115-0228-4e67-ae56-69bc8f81d008

📥 Commits

Reviewing files that changed from the base of the PR and between a10a877 and 9027d14.

📒 Files selected for processing (49)
  • cli/internal/awsapi/awsapi.go
  • cli/internal/awsapi/preauth_test.go
  • cli/internal/awsapi/sigv4.go
  • cli/internal/core/core.go
  • cli/internal/core/targets.go
  • cli/internal/core/targets_test.go
  • cli/internal/httpx/authz.go
  • cli/internal/httpx/httpx.go
  • cli/internal/server/sandbox_test.go
  • cli/internal/server/server.go
  • cli/internal/server/targets.go
  • cli/internal/server/targets_arn_test.go
  • cli/internal/server/workloads.go
  • cli/internal/svc/autoscaling/autoscaling.go
  • cli/internal/svc/autoscaling/passrole_test.go
  • cli/internal/svc/cognito/cognito.go
  • cli/internal/svc/cognito/signup_limit_test.go
  • cli/internal/svc/cognito/srp.go
  • cli/internal/svc/ec2/ec2.go
  • cli/internal/svc/ec2/launchtemplates.go
  • cli/internal/svc/ecs/aws.go
  • cli/internal/svc/ecs/ecs.go
  • cli/internal/svc/ecs/passrole_test.go
  • cli/internal/svc/iam/iam.go
  • cli/internal/svc/iam/login_test.go
  • cli/internal/svc/iam/passrole_spelling_test.go
  • cli/internal/svc/iam/roles.go
  • cli/internal/svc/iam/routes.go
  • cli/internal/svc/lambda/apigw_aws_test.go
  • cli/internal/svc/lambda/apigw_serve.go
  • cli/internal/svc/lambda/apigw_ssrf_test.go
  • cli/internal/svc/lambda/aws.go
  • cli/internal/svc/lambda/lambda.go
  • cli/internal/svc/lambda/zipbomb_test.go
  • cli/internal/svc/s3/aws.go
  • cli/internal/svc/s3/s3.go
  • cli/internal/svc/s3/sigv2.go
  • cli/internal/svc/s3/website_security_test.go
  • cli/internal/svc/secrets/rotation.go
  • cli/internal/svc/sns/aws.go
  • cli/internal/svc/sns/aws_test.go
  • cli/internal/svc/sns/sns.go
  • cli/internal/svc/sqs/aws.go
  • cli/internal/web/web.go
  • cli/internal/web/web_test.go
  • console/app/login/page.tsx
  • console/node_modules
  • docs/install-server.md
  • docs/security-audit-2026-10.md
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying homecloud with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9027d14
Status: ✅  Deploy successful!
Preview URL: https://898f60ed.homecloud.pages.dev
Branch Preview URL: https://security-audit.homecloud.pages.dev

View logs

@drk1rd
drk1rd merged commit cb8641b into main Sep 30, 2026
6 checks passed
@drk1rd
drk1rd deleted the security-audit branch September 30, 2026 19:32
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 30, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant