transloadit / uppy

The next open source file uploader for web browsers :dog:
https://uppy.io
MIT License
29.27k stars 2.01k forks source link

Uncaught TypeError: Cannot read properties of null (reading 'postMessage') in Companion's Callback Endpoint #5334

Closed edanweis closed 4 months ago

edanweis commented 4 months ago

Initial checklist

Link to runnable example

https://stackblitz.com/edit/nuxt-starter-y4aec5

Steps to reproduce

  1. Sign in with Google opened in new tab
  2. Successfully authenticate (https://example.com/companion/connect/googledrive/callback?)
  3. New tab (https://example.com/companion/drive/send-token?uppyAuthToken=long-token-here) does not close and returns the error.

Setup:

My nuxt-starter stackblitz includes my nginx, companion pm2 script and companion.json options (which seems to break CORS, so I fallback to the env variable options). I also tested with Uppy Dashboard starter with the same errors.

Causes I've investigated:

Request

Request URL:
https://example.com/companion/drive/send-token?uppyAuthToken=***long-token-here***
Request Method:
GET
Status Code:
200 OK
Remote Address:
104.21.71.103:443
Referrer Policy:
strict-origin-when-cross-origin

Response Headers

Access-Control-Allow-Credentials:
true
Access-Control-Expose-Headers:
i-am
Alt-Svc:
h3=":443"; ma=86400
Cf-Cache-Status:
DYNAMIC
Cf-Ray:
8a357ac8cec55d32-SYD
Content-Encoding:
br
Content-Type:
text/html; charset=utf-8
Cross-Origin-Opener-Policy:
unsafe-none
Date:
Mon, 15 Jul 2024 00:01:43 GMT
I-Am:
https://example.com/companion
Nel:
{"success_fraction":0,"report_to":"cf-nel","max_age":604800}
Report-To:
{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=c5NhRh2%2BYQamCeT6qNKYnJ5cUQLncY8ClywxcAlDHHV2lK9%2BwEMlewE753xXKrc0ZFGMrktD97E0dl3ttpRqJ0E8l9hOsZ7GFThjVs5ruNxFI0BXfs5Sf0UC8hSof3j3E25bQZ1MWwc%3D"}],"group":"cf-nel","max_age":604800}
Server:
cloudflare
Vary:
Origin
X-Content-Type-Options:
nosniff
X-Download-Options:
noopen
X-Frame-Options:
SAMEORIGIN
X-Powered-By:
Express
X-Request-Id:
7f12b4ac-c0c7-422e-9822-a8e77a23558f
X-Xss-Protection:
0

nginx.conf

user nginx;
worker_processes auto;

error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    types_hash_max_size 2048;
    types_hash_bucket_size 64;

    server {
        listen 80 default_server;
        listen [::]:80 default_server ipv6only=on;

        listen 443 http2 ssl;
        listen [::]:443 http2 ipv6only=on ssl;

        ssl_certificate         /home/ec2-user/full_chain.pem;
        ssl_certificate_key     /home/ec2-user/cloudflare_origin.key;
        ssl_trusted_certificate /home/ec2-user/full_chain.pem;

        # Load custom parameters for Diffie Hellman key exchange to avoid the usage
        # of common primes
        ssl_dhparam /etc/nginx/dhparams.pem;

        # Restrict supported ciphers to prevent certain browsers from refusing to
        # connect because we are offering blacklisted ciphers. This configuration has
        # been generated by Mozilla's SSL Configuration Generator on the
        # intermediate profile and can be accessed at:
        # https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=nginx-1.10.1&openssl=1.0.1e&hsts=no&profile=intermediate
        # More information about blacklisted ciphers can be found at:
        # http://security.stackexchange.com/questions/126775/understanding-blacklisted-ciphers-for-http2
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers 'ECDHE-ECDSA-CHACHA2****************SS';
        ssl_prefer_server_ciphers on;

        # Enable OCSP stapling which allows clients to verify that our certificate
        # is not revoked without contacting the Certificate Authority by appending a
        # CA-signed promise, that it's still valid, to the TLS handshake response.
        ssl_stapling on;
        ssl_stapling_verify on;

        # Enable SSL session cache to reduce overhead of TLS handshake. Allow nginx
        # workers to use 5MB of memory for caching but disable session tickets as
        # there is currently no easy way to rotate the ticket key which is not in
        # sync with the ideals of Perfect Forward Secrecy.
        ssl_session_timeout 1d;
        ssl_session_cache shared:SSL:5m;
        ssl_session_tickets off;

        server_name example.com;

        # certbot will place the files required for the HTTP challenge in the
        # webroot under the .well-known/acme-challenge directory. Therefore we must
        # make this path publicly accessible.
        location /.well-known {
                root /mnt/nginx-www/;
        }

        add_header Cross-Origin-Opener-Policy 'unsafe-none' always;

        location / {
            # Forward incoming requests to local tusd instance
            proxy_pass http://localhost:8080;

            # Disable request and response buffering
            proxy_request_buffering  off;
            proxy_buffering          off;
            proxy_http_version       1.1;

            # Add X-Forwarded-* headers
            proxy_set_header X-Forwarded-Host $host;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_set_header         Upgrade $http_upgrade;
            proxy_set_header         Connection "upgrade";
            client_max_body_size     0;

        }

       location /companion/ {
        proxy_pass http://localhost:3020/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $hostname;
        # WebSocket support (if needed)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_set_header Cross-Origin-Opener-Policy 'unsafe-none';

      }
    }
}

pm2 script

module.exports = {
  apps: [
    {
      name: 'uppy-companion',
      script: '/home/ec2-user/.npm-global/bin/companion',
      args: '--config companion.json',
      args: '',
      instances: 1,
      exec_mode: 'cluster',
      autorestart: true,
      watch: false,
      max_memory_restart: '1G',
      env: {
        NODE_ENV: 'production',
        SESSION_SECRET: '6a9f075920e7b147af29b61cafb3cb5489517e87d14acdabed6126d36c28172e9295f47911bf35e7a8db975d6e390fd93a2283e71dd531fac1a5036d92f93aa4',
        COMPANION_SECRET: '2886664bb4e4c145cd9ab470a6e2071b92023def7c6b527107b307305cb3749feaafe30a6ecc2dff4aaf57008e222a9f9fd0abc961ba194a1af1875ff68683bc1',
        COMPANION_BOX_KEY: '***',
        COMPANION_BOX_SECRET: '***',
        COMPANION_DROPBOX_KEY: '***',
        COMPANION_DROPBOX_SECRET: '',
        COMPANION_GOOGLE_KEY: '***',
        COMPANION_GOOGLE_SECRET: '***',
        COMPANION_ONEDRIVE_KEY: '***',
        COMPANION_ONEDRIVE_SECRET: '***',
        COMPANION_ONEDRIVE_DOMAIN_VALIDATION: false,
        COMPANION_AWS_KEY: '***',
        COMPANION_AWS_SECRET: '***',
        COMPANION_AWS_BUCKET: '***',
        COMPANION_HIDE_WELCOME: true,
        COMPANION_AWS_REGION: 'ap-southeast-2',
        COMPANION_OAUTH_DOMAIN: 'example.com',
        COMPANION_DOMAIN: 'example.com',
        COMPANION_PROTOCOL: 'https',
        COMPANION_PORT: 3020,
        COMPANION_CLIENT_ORIGINS: 'true',
        COMPANION_PREAUTH_SECRET: '',
        COMPANION_OAUTH_ORIGIN: '*',
        COMPANION_SELF_ENDPOINT: 'example.com/companion',
        COMPANION_HIDE_METRICS: false,
        COMPANION_HIDE_WELCOME: true,
        COMPANION_STREAMING_UPLOAD: true,
        COMPANION_DATADIR: '/home/ec2-user/data',
        COMPANION_PREAUTH_SECRET: '6a9f075920e7b147af29b61cd93a2283e71dd531fac1a5036d92f93aa4afb3cb5489517e87d14acdabed6126d36c28172e9295f47911bf35e7a8db975d6e390f',
        COMPANION_UPLOAD_URLS: "['*']",
        COMPANION_PATH: '',
        COMPANION_IMPLICIT_PATH: '/companion',
        COMPANION_DOMAINS: "['example.com']",
        COMPANION_ALLOW_LOCAL_URLS: false,
      },
    },
  ],
};

Expected behavior

As @mifi says:

In the normal auth flow with Uppy: User clicks auth Browser Tab1 (Uppy) pops up another browser Tab2 (Auth flow) Tab2 runs the auth flow with the provider Tab2 auth flow redirects to companion's callback endpoint, which returns HTML that calls window.opener.postMessage(token) to send the token back to Tab1 Tab2 calls window.close() to close Tab2 Tab1 finishes the auth with the received auth token

Actual behavior

New tab (https://example.com/companion/drive/send-token?uppyAuthToken=long-token-here) does not close and returns the error.

Uncaught TypeError: Cannot read properties of null (reading 'postMessage')
    at send-token?uppyAuthToken=***long-token-here***~:7:25

New Tab source

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8" />
        <script>
          window.opener.postMessage({"token":"***long-token-here***"}, "https:\u002F\u002Flocalhost:3000")
          window.close()
        </script>
    </head>
    <body></body>
    </html>
mifi commented 4 months ago

Hi. does this also happen when you don't use stackblitz? (e.g. local development) i have a theory that it happens because in stackblitz the app runs inside of an iframe

edanweis commented 4 months ago

@mifi yes it happens in local development, stackblitz and in production. I forgot to mention I am also using Cloudflare with DNS proxy

mifi commented 4 months ago

ok thanks for clearing that up.

  1. Does it happen in all browsers for you?
  2. Do you mean that you're using cloudflare with DNS proxy in front of Companion or for the Uppy static code?
  3. If so, does it happen if you by-pass cloudflare and connect directly to companion?
  4. Does cloudflare forward all headers from companion?
  5. Does it happen if you run Companion on localhost and connect to it using local Uppy?

Where is Companion hosted? I don't know how your stackblitz can possibly work because it uses https://example.com/companion

I think it could be related to #4107

mifi commented 4 months ago

also have you set a Cross-Origin-Opener-Policy header?

mifi commented 4 months ago

I can see that Cross-Origin-Opener-Policy: same-origin does get set when running from StackBlitz. So it won't work there.

Having a Cross-Origin-Opener-Policy header with a value of same-origin prevents setting opener. Since the new window is loaded in a different browsing context, it won't have a reference to the opening window.

from https://developer.mozilla.org/en-US/docs/Web/API/Window/opener ​ Are you setting that header when testing locally and in production?

edanweis commented 4 months ago
  1. Error happens in Chrome/Incognito and Firefox
  2. Cloudflare with DNS proxy in front of Both companion and uppy static code.
  3. Turning off Cloudlfare DNS proxy causes net::ERR_CERT_AUTHORITY_INVALID when Uppy code communicates with the companion server
  4. Headers are pasted as above. I'm unsure how to examine this further
  5. Just reproduced the error running Companion on localhost and connecting to it using local Uppy
  6. Yes, in production you can see add_header Cross-Origin-Opener-Policy 'unsafe-none' always; in my nginx config pasted above. I haven't tested that locally. Should companion do that by default?
mifi commented 4 months ago

Are the Uppy web-app static files hosted in nginx also (the configuration above)? If not, can you check whether the request to get the webapp HTML has a Cross-Origin-Opener-Policy header in the response? (for example using chrome developer tools Network tab)

edanweis commented 4 months ago

No they are hosted by Vercel, or locally in Nuxt3 Nitro server. example.com is a redaction. All headers were being sent.

I think I solved it, the problem was the nuxt-security module I am using:

security: {
    nonce: true,
    corsHandler: {
      origin: process.env.AUTH_BASE_URL,
      methods: "*",
    },
    headers: {
      crossOriginEmbedderPolicy: false,
      contentSecurityPolicy: {
        "script-src-attr": ["'unsafe-hashes'", "'unsafe-inline'"],
        "img-src": false, //["'self'", 'data:'],
        "script-src": [
          "'self'",
          "https:",
          "'unsafe-inline'",
          "'strict-dynamic'",
          "'nonce-{{nonce}}'",
          "'unsafe-eval'",
        ],
      },
    },
  },

The stackblitz same-origin must have been confounding my tests with the Uppy vue template. Thanks for your time @mifi

mifi commented 4 months ago

alright, so Cross-Origin-Opener-Policy: same-origin was the problem, and we close this? I think we should provide a better error message (not just a blank page)

edanweis commented 4 months ago

Yes that was the problem. I'll close the issue