Adieu, QR Code Menu Ado
With the increasing prevalence of QR code-based menus, there have been a growing number of articles discussing privacy concerns. Personally, I've noticed that some of the QR codes I encounter first direct me to a third-party website before redirecting me to the website of the restaurant in question. While this apparently might be done to enable "dynamic" QR codes (where the target of the QR code can be changed after the fact), this bothers me because it introduces a more centralized third party that has the potential to track patrons across multiple restaurants.
Granted, addressing this issue does nothing to tackle tracking by third-party restaurant platforms (as might be used for online or mobile ordering). However, I would argue that maintaining decentralized access to restaurant websites still improves privacy, especially in instances where such platforms are not used and the websites are instead standalone or, even better, in PDF form. To date, I have only encountered one restaurant that uses a third-party platform for dining in, though I have continued to be a bit of a hermit with the ongoing pandemic so my experience is not likely to be representative.
Anyways, enough with the "what," on to the "how"
This project started by looking specifically at qrco.de, as most of the third-party QR codes I've encountered are for that domain (and who could forget such a memorable domain name). One of the goals going into this project was to have minimal impact on the usual QR code workflow. I really like that the scanner is integrated into the camera on iOS.
My initial thought was to do some DNS-based request hijacking to intercept requests for qrco.de (and others, as I happen across them) and process them at a server internal to my home network. Turns out qrco.de provides a 302 Found
response to redirect requests, which could be processed server-side in a way that doesn't store any cookies (hello, curl -I
). The target URL could then be provided in response to the intercepted request.
I initially liked this option, because it would enable more complex processing as needed, for example if a site has a landing page prior to redirection. It also sounded like a lot of interesting problems to solve. For example, qrco.de appears to typically use HTTPS, so I would need to generate a TLS certificate using my personal certificate authority. I would also likely do the interception on the same (virtual) machine that serves my Pi-hole, so I was thinking I would need another IPv4 address to receive the intercepted requests. Downsides of this plan? I would need to always be on my VPN and I think it would likely be less robust.
And so, my focus instead turned client side. How cool would a QR scanner iOS app be?! But seriously, there is a certain amount of sense to having a privacy-minded QR code scanner app. I haven't done any iOS / macOS app development for the better part of a decade (and a whole application does seem a little ham-handed), so that option wasn't a serious one. However, what was a serious option was Shortcuts.
But seriously, the "how"
I think I first looked at Shortcuts becuase I was thinking I could call a script via SSH (to run the curl
command) and then open the result in a browser. A little clunky, but it would have worked. Thankfully, it turns out Shortcuts offers an "Expand URL" function, which "expands and cleans up URLs [that] have been shortened using a URL shortening service." Perfect.
I was curious to see how the "Expand URL" function worked, namely whether it maintainied cookies across invocations. It wouldn't do me much good if I were resolving redirects and passing them to Safari, all the while unknowingly collecting cookies.
So I did what any self-actualized human bean would do and set up a dummy webserver. With some help from my frriend php -S [bind addr]:80
, I wrote a simple PHP script to dump cookies and view requests from my phone.
<?php
// Launch PHP server with 'sudo php -S [bind addr]:80'
// Simple console debugging
error_log("\n");
error_log(implode($_SERVER,", "));
error_log(implode($_COOKIE,", "));
// Borrowed function: "based on original work from the PHP Laravel framework"
if (!function_exists('str_contains')) {
function str_contains($haystack, $needle) {
return $needle !== '' && mb_strpos($haystack, $needle) !== false;
}
}
// Name to easily change the cookie we're setting
$cookieName = "trackingCookie";
// If we are accessing the tracking URL (i.e., '?trackingURL'), set a cookie and redirect
if(str_contains("$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]", "trackingURL")) {
setcookie($cookieName, time(), time() + 3600, '/');
header("Location: http://$_SERVER[HTTP_HOST]/?destination");
// If we are accessing a normal URL (e.g., '?destination'), check for tracking cookie
} else if(isset($_COOKIE[$cookieName])) {
echo "Cookie '" . $cookieName . "' has been set!<br>";
echo "Value in cookie is: " . $_COOKIE[$cookieName];
} else {
echo "No cookie.";
}
?>
Super simple. The important bits are where a cookie is set if the URL is accessed with ?trackingURL
and then a redirect occurs (i.e., to ?destination
), at which point the destination page prints whether a cookie was actually set. Ideally, the "Expand URL" function would eat the cookie, such that Safari arrives unfed at the ?destination
URL.
Onwards, to the results!
When the ?trackingURL
URL is called via the Shortcut, a User Agent of BackgroundShortcutRunner/1137.4 CFNetwork/1312 Darwin/21.0.0
is used to access the page. The cookie is then set, the redirect to ?destination
is followed, and the ?destination
URL is ultimately returned by the function. Not really relevant, but I assume that the function follows the ?destination
URL to see if there is yet another redirect, which is good to see. Also, it makes sense to collect cookies along the way in case they are needed at the destination URL.
The important part: when the ?destination
URL is accessed in Safari (with a User Agent of Mozilla/5.0 (iPhone; CPU iPhone OS 15_0_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1
), it is apparent that no cookie is set yet. So, yes! The "Expand URL" function will work to protect from tracking cookies coming from QR code redirects. Super fucking yay.
But wait: another Shortcuts function?
I should mention that there was another candidate function that I encountered when I was evalauting options. Hellooooooo "Show Web Page." Just imagine: you scan a QR code, get redirected to whatever website in a WebKit view, and then, once you're done, you can close the view and all the cookies are gone with it. Bango bongo, right?
Sadly, not. Interestingly, it turns out that the "Show Web Page" function and Safari share cookies. That means that, unlike "Expand URL," "Show Web Page" can't be used to follow redirects and happily discard cookies at the end of the journey. What a world it almost was, though.
Imple-menu-tation
Enough lollydicking, finally it's actually time the "how." The implementation I arrived at is a Shortcut that either parses a URL (obtained via the Share Sheet) or pops up a QR code capture screen, "Expand[s that] URL," and passes the resulting URL to Safari. This workflow is further simplified by tying the Shortcut to Back Tap, so I can easily scan the QR code through the Shortcut rather than the Camera app.
Conclusion
Is it perfect? No. But it is serviceable, at least to a point. It does not address whatever tracking cookies might be waiting at the destination, which seems like a logical step for implementing tracking across a number of dining establishments. For that, I will rely on pihole-based DNS filtering and whatever tracking protections are offered by Safari, at least for now.
Which brings me back to my initial comment about a privacy-minded QR scanner. It feels like scanning QR codes involves a certain reduction in control. In closing, it would be really nice if QR code interactions could basically be sandboxed, which would likely make the most sense as an OS-level feature. Even the ability to open a private tab in Safari via Shortcuts would be helpful. Alas, a pipe can dream. Until that day, I guess "Expand URL" it is.