Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
2 tasks done
edanweis opened this issue Jul 15, 2024 · 10 comments
Assignees
Labels

Comments

@edanweis
Copy link

edanweis commented Jul 15, 2024

Initial checklist

  • I understand this is a bug report and questions should be posted in the Community Forum
  • I searched issues and couldn’t find anything (or linked relevant results below)

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:

  • Standalone version of Companion
  • pm2
  • nginx
  • AWS EC2 server
  • Nuxt3

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:

  • Settting CORS Access-Control-Allow-Origin header via comma separated strings in COMPANION_CLIENT_ORIGINS so that the targetOrigin for postMessage calls are available in the context of OAuth.
  • nginx CORS not being set (they are set by standalone Companion)
  • nginx not passing some headers through
  • Uppy auth tokens not being sent via WSS (send uppyAuthToken via wss  #4110 )
  • Headers from https://example.com/companion/drive/send-token? incorrect

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
Copy link
Contributor

mifi commented Jul 15, 2024

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
Copy link
Author

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

@mifi
Copy link
Contributor

mifi commented Jul 15, 2024

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
Copy link
Contributor

mifi commented Jul 15, 2024

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

@mifi
Copy link
Contributor

mifi commented Jul 15, 2024

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
Copy link
Author

  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
Copy link
Contributor

mifi commented Jul 15, 2024

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
Copy link
Author

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
Copy link
Contributor

mifi commented Jul 15, 2024

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)

mifi added a commit that referenced this issue Jul 15, 2024
when window.opener == null
#5334 (comment)
@edanweis
Copy link
Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants