You need to make your function handle a range. One way of accomplishing this would be like this:
vnoremap <leader>rl :call VisAddRefLink()<CR>
function! VisAddRefLink() range
exe a:firstline . "normal! ^i["
exe a:lastline . "normal! $a]"
endfunction
The reason your example isn't working is because the exe doesn't operate on a visual selection per say. For example, try visually selection something and then doing :norm d. You'll notice it doesn't delete. If you add a range to your function like :help function-range-example it helps with visual selections by operating in a similar fashion (line by line). However, it still isn't a true visual selection. The range addition does allow you the a:firstline and a:lastline variables though, which can be used to accomplish this. You can also accomplish this with a single like in this manner:
vnoremap <leader>rl <esc>:norm! '<^x2Phr['>$x2pr]<cr>
This first uses <esc> to end the visual selection. Then it executes a normal command that will only run once. If the visual selection was left then it would run once for each line in the visual selection. Once it's run once it
'<^ jumps to the first line of the visual selection and to the first non-blank space on that line.
x2Phr[ deletes that character, pastes it twice in front, moves to the left so we're over the new character, and replaces it with the opening [
'>$ move to the last character on the last line of the visual selection
x2pr] same as before but in the opposite direction
As usual, there's more than one way to skin a cat, especially with vimscript. As you learn more you see a lot of possibilities for accomplishing things.