So I guess I have to use a Token-URL-Parameter
This is a sensible (if imperfect) option. You need to have a secret shared between the two domains which is not shared with the browser.
This can be done via a database, with two methods of viable paranoia.
Method one: Randomness
Setting:
domain1.com
sets a secret key (see below) and this value is stored in the database along with authentication criteria such as IP address, datetime, a hash of the browser identification ($_SERVER['HTTP_USER_AGENT']
), and whatever else you want to use to be more selective with validating the secretkey is not just a lucky fluke.
The database should ideally be on the same server as the domain(s) and be accessed locally only. But this is a sliding scale as to how secure you really want to be and how much time/money/resources you want to invest in this topic.
This secret key is passed to domain2.com
via your (urlencode
d) GET parameter as you illustrate.
base64_
is not required, but if you want to use it there's no harm. It's better (required) to use urlencode($variable)
when setting the secretkey in the link URL.
NOTE: you do not need to use urldecode()
on the received $_GET
variable.
- How to generate an individual secretkey?
The secret key should not contain any of the above identifiable information, and be purely random. It should be long. Another answer here quotes 8 characters. This will be broken almost instantly. You want to generate a key string as long as possible. you may only be limited by the indexing system on the VARCHAR
column of the database where the same secretkey is stored. so you want a key of 184 characters, when fully decoded.
Use random_bytes()
or openssl_random_pseudo_bytes()
, depending on your version of PHP.
run this on a loop to make a key length and using the examples from the manual pages above to set this to a alphanumeric string using bin2hex
or similar.
Do not use SHA1 or MD5 to generate hashes.
("Why not, sherlock?")
Once you have the secretkey you then need to check if it's in the database. The easiest way is to build the database so that the secretkey column is a UNIQUE INDEX
(MySQL) and so if there is repetition this will not insert. You must build logic so that if the key does not insert then you should restart and build a new key.
When saving the key to the database you should also save other meta- data as refrenced above, IP addresses, browser hashes, datetime, etc.
Once this process is complete you can then move on to checking the URL ey given is valid.
Getting:
- Validating the Random string:
You have the key in your $_GET
PHP value. Pass this key to your database (of course, using Prepared Statements) and check it exists.
- Validating and qualifying the meta-data:
Once you find it exists, you can the check the meta details and confirm they're correct. For example that the time that the request is called is not more than x number of minutes after the datetime value in the database for row creation.
Which inconsistencies you check for and which you react to is down to you, but by example it's worth checking the browser hash is the same so that you can be fairly sure that one browser used to access the domain1.com
is the same browser as accessing domain2.com
.
- How to deal with inconsistencies?
So, a browser requests a string that doesnt exist? Bang a 5 second sleep()
statement on that condition before kicking out to the homepage. A browser asks for a historic string (old datetime value), bang a sleep(5)
on that before kicking out to the homepage. etc. etc.
If you wish, you can record the IP addresses of failed attempts and use this to count (in another database table) any IP blocks that are repeatedly trying to crack your hashes then, you can use more code to add a time penalty such as if 205.453.345.xxx
IPs have 25 failed attempts, you can then add an additional sleep()
to any access from that IP, or even for higher numbers simply kick that IP block ot without even bothering to check the hash.
Do not feedback to the user/browser the reason why any particular hash token failed. A probing attacker can use this data to work out more about your hash or meta data checking process.
Warning: IPs
Be extremely careful with blocking or excessively pausing IPs as who uses IP addresses is constantly changing. If you keep counts of IP failures by block [123.456.678.xxx]
or on a per-address basis, you should not store counts for longer than say 7 days. otherwise IPs that historically belonged to hackers a month ago will then belong to genuine users who are still suffering penalties throgh no fault of their own.
Storing IP failures on a per-address basis is costly in diskspace and relatively useless. Block tracking is more efficient.
Once you've verified the secretkey, and the meta data is consistent enough you're happy with it, set a cookie/session on the domain2.com
to allow that browser the access and then delete the hashkey from the database, retaining its unique access ability.
IMPORTANT:
Everything noted above can make the best of an imperfect situation. The comment by KIKO Software should be heeded:
You cannot make a secure system by only providing an URL parameter, see: "https url with token parameter how secure is it?" I know it's not exactly what you're doing, but the idea is the same. This problem can be solved, but it requires a lot of explanation and cautions.
There is a LOT more to this specific topic than exampled in this answer, bt it takes dedication, reading and learning. You will NEVER have a perfectly secure system (it's like trying to catch a cloud) so you need to think about what your risks are, how important they are and what time and effort it's worth finding mitigations.
Most security is not about solutions to problems but mitigating problems. In the above example it can be possible to compromise a certain secretkey but the likelihood of it -- comrpomising a key in the allowed time window and with a perportedly equviliant browser and using a same IP address -- is very small, but is absolutely not impossible.
Method two: Encryption.
Not recommended. Encryption is processing power heavy and is the same as above but has a much bigger attack window.
The only reasoning I can see for using encryption here is if for some foolish reason you're unable to use a database. Therefore you can make a cyphertext of the metadata (IP, date, time, etc) and then send this across to the domain2.com
for the PHP on domain2.com to check the cyphertext's compare.
Say, for instance the meta data you would normally put is:
$plainText = $_SERVER['REMOTE_ADDR'].date("Y-m-d").md5($_SERVER['HTTP_USER_AGENT']);
$options = [ 'cost' => 12,];
$cypher = password_hash($plainText, PASSWORD_BCRYPT, $option );
$urlCypher = urlencode($cypher);
And to read it at the domain2.com
you would use:
$checker = $_SERVER['REMOTE_ADDR'].date("Y-m-d").md5($_SERVER['HTTP_USER_AGENT']);
if(password_verify($checker,$_GET['keystring'])){
//values compare ok.
}
While this works, it can also work for any remote computer who fills in the correct cypher details, because all parts of the data checked are already known to the end user browser.
There is no independent observer, unlike with the database scenario.
This method is not recommended as it is processor heavy and potentially more vulnerable
Some source links: