Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Tue, 23 Jul 2024 22:37:42 +0000 en-US hourly 1 https://wordpress.org/?v=6.6.1 225069128 The Pitfalls of In-App Browsers https://frontendmasters.com/blog/the-pitfalls-of-in-app-browsers/ https://frontendmasters.com/blog/the-pitfalls-of-in-app-browsers/#comments Wed, 17 Jul 2024 16:07:47 +0000 https://frontendmasters.com/blog/?p=3008 Developing websites for modern mobile devices has a pitfall you may not be aware of: in-app browsers. These are web browsers embedded directly within native mobile apps. So if a link is clicked within a native app (e.g. Instagram or TikTok), it uses the in-app browser instead of switching apps to a dedicated browser app.

While potentially convenient for mobile developers (i.e. users will never leave our app! the businessmen squeal), we’ll discuss the drawbacks for web developers like yourself and your users.

In-app browsers are also referred to as embedded browsers or WebView. These are interchangeable terms.

The Drawbacks

The drawbacks of in-app browsers can be broadly categorized:

Limited functionality

In-app browsers are considerably stripped down when compared to their fully-featured counterparts and typically lack features like bookmarking, UI controls, settings, extensions, and downloads. For for instance a browser extension that a user depends on or help protect their privacy will not work in an in-app browser.

Privacy & security concerns

Because in-app browsers are embedded within a native mobile app, the app developer has control and visibility into the users’ in-app browsing activity. This even extends into being able to inject code into the in-app browser which is a major privacy and security concern. Users are largely unaware and aren’t able to opt-out even if they are.

Inconsistent UI/UX

Because in-app browser implementations are all different, the UI is inconsistent. Further, browsing data like history and bookmarks aren’t shared so users typically need to sign into services they may already be securely signed into in their devices actual browser. This leads to a fragmented and frustrating user experience.

Worse performance

In-app browsers tend to be running outdated browser internals which can cause slower loading times and compatibility issues. Users on slower Internet connections may have the problem exacerbated.

Author Update: Since Apple doesn’t allow apps, even browsers, to use their own rendering engine only Android has the problem of bundling in a custom in-app browser instead of using the system WebView, which may be outdated and have worse performance. On iOS, the built-in WebView is bundled as part of the iOS WebKit. On Android, the default built-in WebView is based on the Blink version and is updated independently of the OS as part of the Chrome update process via Google Play.

Bad Behavior in In-App Browsers

History

In-app browsers have existed since circa 2016, but it wasn’t until 2019 when Thomas Steiner, a Google engineer, published a blog post that dove into Facebook’s iOS and Android apps that a wider audience was made aware of the privacy and security concerns. Thomas discussed the technical details behind how the apps implemented their in-app browsers and stated how in-app browsers can perform man-in-the-middle (MITM) attacks by injecting arbitrary JavaScript code and intercepting network traffic.

Three years later, in 2022, Felix Krause published two blog posts (1, 2) and a tool, inappbrowser.com, which focused on the privacy concerns of iOS apps. Initially this covered apps by Meta (Facebook, Messenger, Instagram) and then followed up with Android and other social media apps including TikTok. Felix’s findings supported Thomas’ from 3 years earlier and showed concerning findings from iOS Instagram: the arbitrary injection of a pcm.js script which Meta claimed to be an “event aggregator” but was also monitoring user interactions in the form of taps and selections. Further cause for concern was TikTok injecting JavaScript that monitors all keyboard inputs along with taps, which is effectively the functionality of a keylogger on third-party sites. TikTok acknowledged the existence of this code but claimed it’s only used for debugging, troubleshooting, and performance monitoring.

Felix’s findings led to a lawsuit being filed against Meta in September 2022. The case was dismissed in October 2023.

Nothing Has Changed

Let’s revisit the behavior of iOS & Android Instagram’s in-app browser at the time of this writing (July 2024). This is done by sharing the two testing links, inappbrowser.com and inappdebugger.com (we’ll discuss this one more shortly), in the app as a direct message or URL in your profile bio. This is so you can actually click on them, as Instagram prevents clickable URLs in places like the descriptions of posts.

Let’s start with iOS. Below is iOS Instagram opening inappbrowser.com and inappdebugger.com in July 2024:

This shows that iOS Instagram is still injecting arbitrary JavaScript code which listens to user clicks along with JavaScript messages.

(Editor note: when testing this I noted that Instagram also appends URL parameters on outgoing links, which may be used to communicate additional information to this injected JavaScript).

Next, Android.

The story on Android is slightly different: there’s still arbitrary JavaScript being injected but it isn’t necessarily listening to events tightly coupled with user interactions.

Unfortunately, not much has changed since Felix’s findings nearly 3 years ago.

Open Web Advocacy wrote a piece earlier this year following the events of Apple threatening to kill web apps.

Debugging & Detecting In-App Browsers

Leveraging the existing excellent work of Felix Krause and Shalanah Dawson we have strategies for debugging and detecting when our websites are being viewed by in-app browsers.

  • https://inappbrowser.com/
    • Attempts to detect if there’s any injected JavaScript code running.
  • https://inappdebugger.com/
    • Attempts to detect you’re in an in-app browser and, if so, which app it is inside of.
    • Additionally provides some debugging tests for if downloads are possible and escape-hatches for getting to an actual device browser.
    • Leverages both inapp-spy and bowser.
  • https://github.com/bowser-js/bowser
    • A browser detection library providing metadata and filtering based on browser version.
  • https://github.com/shalanah/inapp-spy
    • A TypeScript library written by Shalanah Dawson that aids in detecting in-app browsers.

Escaping

Now that we have some tools, let’s look at a example in JavaScript for detecting and redirecting in Android using an intent: link. You’d do this if you simply do not want your website being opened in an in-app browser, and offer a link to users to open it in their default browser instead.

import InAppSpy from "inapp-spy"
const { isInApp } = InAppSpy()

// Your app's full URL, maybe defined build-time for different environments
const url = `https://example.com`
const intentLink = `intent:${url}#Intent;end`

// 1. Detect in-app
if (isInApp) {

  // 2. Attempt to auto-redirect
  window.location.replace(intentLink)
    
  // 3. Append a native <a> with the same intent link
  const $div = document.createElement("div")
  $div.innerHTML = `
    <p>Tap the button to open in your default browser</p>
    <a href="${intentLink}" target="_blank">Open</a>
  `
  document.body.appendChild($div)
}

It’s not ideal to have to load extra JavaScript for this, but it is reliable. This may be heavy handed, but for those of you working on particularly sensitive sites, it might be worth doing.

To get an idea of a way this can be implemented, Shalanah’s inappdebugger.com provides this functionality under the “Android In-App Escape Links” section.

Test out the Android escape hatch strategy on inappdebugger.com.

Unfortunately, there’s currently no reliable way of handling iOS in-app browsers in terms of a proper escape hatch. Similar to Android, there’s a handful of device-specific URI schemes (that’s technically what the intent: prefix is called), but none of them are reliable to the default browser on a specific URL. A not-so-great workaround is using the x-web-search://? scheme, but the best-case is using the site: search prefix to get close to your actual URL e.g. x-web-search://?site:example.com.

Author Update: a somewhat reliable iOS workaround has been documented and tested by trying to run a Shortcut that doesn’t exist, specifying your URL in an error callback, and opening that in the user’s default browser. In practice, this looks like:

shortcuts://x-callback-url/run-shortcut?name=${crypto.randomUUID()}&x-error=${encodeURIComponent('https://example.com')}

This comes with some side effect caveats: the Shortcuts app is opened on the user’s device and some query parameters are appended to your URL. Read more on GitHub.

A last-ditch effort on iOS would be creating a UI element in your web app that gives the user manual instructions for bailing:

  1. Tapping the “…” menu
  2. Tapping on “Open in browser”
Screenshot of a UI that points to the upper right of the screen saying:

1. Click on the overflow menu (•••)
2. Then click Open in browser

This is considerably more fragile and error-prone, but if you have the metrics to where your user traffic is coming from and which in-app browser is preventing them from converting to your feature-rich PWA then it could be worth considering.

Hopefully, with time, we’ll see the fall of in-app browsers. The privacy and security concerns alone are unacceptable. Couple that with the limited functionality and poor user experience, it’s probably best they just went away. Thanks to groups like the Open Web Advocacy and individuals like Shalanah Dawson and Felix Krause for their work and support for this cause.

Recommended Reading

]]>
https://frontendmasters.com/blog/the-pitfalls-of-in-app-browsers/feed/ 1 3008
Script Integrity https://frontendmasters.com/blog/script-integrity/ https://frontendmasters.com/blog/script-integrity/#respond Fri, 05 Jul 2024 16:22:36 +0000 https://frontendmasters.com/blog/?p=2923 There was lots of news recently about the polyfill.io project. This was a popular project a few years back, as you could just link it up and it would polyfill whatever web platform features it needed to based on feature testing and UA sniffing. If you loaded the script up from their domain, as the homepage suggested you did, you might have been served some malicious code.

Users are being redirected to sports betting websites or adult domains, likely based on their location, the threat intelligence firm said.

SecurityWeek

The project is offline now, but as of June 1st, 2024 this is how they suggested hotlinking.

When you link to any resource on a domain you don’t control, it’s a risk. It can be a calculated risk. For example, tons of websites load a script from google.com for their Google Analytics. Or load fonts from adobe.com for their typography. Those tend to be trusted sources, but that trust is entirely up to you.

It’s entirely possible that a resource you link to from a third-party disappears or changes. Worst case: changed maliciously, like we’ve seen here. You can bet your ass Google and Adobe don’t load any resources, especially JavaScript, from third-party domains they don’t control.

Protection Against Changes

There is a web platform feature that can help against a third party changing the code they are providing. It’s the integrity attribute on <script> or <link> elements (which are rel="stylesheet"rel="preload", or rel="modulepreload"). It’s called “Subresource Integrity”, to name it correctly.

So for example…

<script
  src="https://third-party.com/script.js"
  integrity="sha384-[hash value here]">
</script>

Now if script.js changes at all, even just a single character, the browser will refuse the execute the script (or stylesheet). Safety!

Some responsible third-parties will offer this directly, which is nice to see.

instant.page versions their script and provides the integrity attribute for safety.

CDNjs makes this a part of the default code you copy and paste.

I particularly like how the protection integrity provides protects against some possible middleman attacks as well. Say your weird hotel WiFi intercepts requests and changes the response (I’ve seen it!), this will prevent the tampered-with script from executing. Uh, well, unless they tamper with the HTML and change the attribute value, which is certainly possible as altering HTML is exactly what invasive in-app browsers do. Still, most security is a do-as-much-as-you-can game and this helps.

When Not to use Script Integrity

The above two examples are kinda perfect as the scripts they are linking to are versioned. They are published with that exact version and that version will never change. In this case, it’s by virtue of strong conventions. When public libraries update, the code at that version is locked in stone. Any changes cause a version update. If the code of a version changes without a version change, that would be highly suspicious, probably malicious, and a great situation for <script integrity="..."> to block. Plus, the major places where libraries are published (i.e. npm) literally don’t allow changes to published versions anyway.

While integrity is often an excellent idea, it’s specifically for versioned resources that don’t change. You would not use it if:

  1. You’re linking to a resource that is OK to change
  2. You’re providing a resource that you intend to change

Perhaps you’re using a script from some analytics service provider. It’s pretty likely that they don’t use the integrity attribute when they give you the script they want you to add. That’s likely because they want to be able to actively work on this script and what it does without having to tell every single customer they need to update the version of the script otherwise it will stop working.

Ironically, a company being able to update a script on the fly means they could potentially patch a security problem.

Would Script Integrity have Stopped the Polyfill.io Problem?

Maybe! It depends what they did. Some reporting said that:

The malicious code dynamically generates payloads based on HTTP headers, activating only on specific mobile devices, evading detection, avoiding admin users and delaying execution. The code is also obfuscated.

So: yes. If what was happening is that the initial response/content of the script was changed from what it was when the integrity value was created, it would have prevented these malicious changes from running.

But they could have evaded this kind of stoppage.

They way that polyfill.io worked already was that it loaded additional content — the polyfills themselves — as needed. That additionally-loaded content could have been changed to be malicious and would not have been subject to subresource integrity. I’m not trying to make bad guys lives easier here, just sayin’.

How To Do It Yourself

You don’t need a third-party to hand you this attribute to use it. It’s just a web platform feature, so you can use it if you want to.

Maybe the easier way is to go to the SRI Hash Generator website, pop in the URL of a resource you want to protect, and hit the button to get the code:

NOTE: I’ve seen plenty of places recommend this site, but when I used it, it didn’t seem to work for me. For example, the exact code above:

<script src="https://assets.codepen.io/3/log-something.js" integrity="sha384-ZTxYWn5UcuOi7Xt1wWg/QFcsZJEXjJg7tgCnVbx0+ssBvbi1Sw/hY5GzPGHSD1NW" crossorigin="anonymous"></script>

That fails for me in Chrome. I had to let Chrome error with a message in the console, which provides the correct hash in the error message, and use that corrected hash for it to work:

<script src="https://assets.codepen.io/3/log-something.js" integrity="sha384-H7W+IxM2qbwMSJYRqmcgYXq0TRko/BIFtURUjh2QG0Z8MM9I6t2f4n+2BOCmbjvD" crossorigin="anonymous"></script>

So… your milage may vary. Here’s the proof on all that.

It’s also worth noting that CORS is involved. Without that crossorigin="anonymous" on there, I was seeing a CORS error with that code above, even though we do serve assets with a Access-Control-Allow-Origin: * header. Shrug — websites are hard.

]]>
https://frontendmasters.com/blog/script-integrity/feed/ 0 2923